From 61ebaad27191af90d68e5e7cbed4e0196b9cbc57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20S=C3=B8derlind?= Date: Sat, 8 Aug 2026 01:11:10 +0200 Subject: [PATCH] prepare-wordpress: PHP test + phpcs.xml + ESLint scaffolding; composer.json hardening; plugin 1.1.0 --- .well-known/agent-skills/index.json | 2 +- .../agent-skills/prepare-wordpress.tar.gz | Bin 14880 -> 16309 bytes plugins/wordpress-skills/CHANGELOG.md | 13 +++ plugins/wordpress-skills/plugin.json | 2 +- .../skills/prepare-wordpress/SKILL.md | 59 ++++++++++++-- .../references/composer-setup.md | 9 ++- .../references/linting-setup.md | 76 ++++++++++++++++++ .../scripts/detect_project.mjs | 20 +++++ .../prepare-wordpress/scripts/plan_setup.mjs | 11 ++- scripts/build-agent-plugin.mjs | 15 +++- skills/prepare-wordpress/SKILL.md | 59 ++++++++++++-- .../references/composer-setup.md | 9 ++- .../references/linting-setup.md | 76 ++++++++++++++++++ .../scripts/detect_project.mjs | 20 +++++ .../prepare-wordpress/scripts/plan_setup.mjs | 11 ++- 15 files changed, 361 insertions(+), 21 deletions(-) create mode 100644 plugins/wordpress-skills/skills/prepare-wordpress/references/linting-setup.md create mode 100644 skills/prepare-wordpress/references/linting-setup.md diff --git a/.well-known/agent-skills/index.json b/.well-known/agent-skills/index.json index 18cb954..3005c0d 100644 --- a/.well-known/agent-skills/index.json +++ b/.well-known/agent-skills/index.json @@ -34,7 +34,7 @@ "type": "archive", "description": "Phase-based WordPress project setup workflow with dry-run planning and confirmed apply for predictable scaffolding/standardization.", "url": "https://soderlind.no/.well-known/agent-skills/prepare-wordpress.tar.gz", - "digest": "sha256:28fd8747a249eddb8308c4d3eb5bfe730db6a3470ab169092fe506b6a1fd26f2" + "digest": "sha256:c0c7545687f6f8e7cb6e322b2ffde7ea87eface20008592031e7ba47f06d7b1a" }, { "name": "wp-bump", diff --git a/.well-known/agent-skills/prepare-wordpress.tar.gz b/.well-known/agent-skills/prepare-wordpress.tar.gz index 43d8ceb7a7414338038d7cf19fdd01e91674ef5b..12f66625d77b46430c0a526d6fdf365391d97bab 100644 GIT binary patch literal 16309 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

K+;$ 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 literal 14880 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 S|33b0`uo3|c^&2e-~a%ut6J#* diff --git a/plugins/wordpress-skills/CHANGELOG.md b/plugins/wordpress-skills/CHANGELOG.md index 8bbf43e..43cbf3d 100644 --- a/plugins/wordpress-skills/CHANGELOG.md +++ b/plugins/wordpress-skills/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to `wordpress-skills` are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.0] - 2026-08-08 + +### Added + +- prepare-wordpress: unit tests mock WordPress with Brain Monkey (brain/monkey); added references/php-testing.md and PHPUnit test scaffolding (phpunit.xml.dist, tests/bootstrap.php, tests/TestCase.php). +- prepare-wordpress: ESLint (@wordpress/eslint-plugin) and a committed phpcs.xml ruleset via a new linting-setup.md reference. +- prepare-wordpress: new instructions phase downloads the github/awesome-copilot WordPress coding instructions. + +### Changed + +- prepare-wordpress: Composer and npm dependencies install the latest compatible releases (unpinned). +- prepare-wordpress: composer.json is written as a file instead of via composer init with user-provided strings. + ## [1.0.0] - 2026-08-07 ### Added diff --git a/plugins/wordpress-skills/plugin.json b/plugins/wordpress-skills/plugin.json index b202eef..61d225a 100644 --- a/plugins/wordpress-skills/plugin.json +++ b/plugins/wordpress-skills/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "wordpress-skills", - "version": "1.0.0", + "version": "1.1.0", "description": "WordPress development, testing, and release skills.", "author": { "name": "Per Soderlind", diff --git a/plugins/wordpress-skills/skills/prepare-wordpress/SKILL.md b/plugins/wordpress-skills/skills/prepare-wordpress/SKILL.md index 1af34c5..a1bb011 100644 --- a/plugins/wordpress-skills/skills/prepare-wordpress/SKILL.md +++ b/plugins/wordpress-skills/skills/prepare-wordpress/SKILL.md @@ -2,7 +2,7 @@ name: prepare-wordpress description: "Phase-based WordPress project setup workflow with dry-run planning and confirmed apply for predictable scaffolding/standardization." compatibility: "macOS/Linux with Node.js 18+, Composer 2+, PHP 8.3+, git. Optional: WP-CLI for i18n commands, curl for downloading coding instructions." -version: "1.2.0" +version: "1.3.0" --- # Prepare WordPress Project @@ -61,6 +61,7 @@ Hard gate: Do not run any `--apply` command until dry-run output is shown and th - `composer` — install PHP dev deps and merge scripts - `config` — create/merge `.editorconfig` and `.gitignore` - `vitest` — install and scaffold Vitest (JavaScript test runner) +- `eslint` — install and configure ESLint for JavaScript - `i18n` — scaffold i18n files/scripts - `instructions` — download WordPress Copilot coding instructions - `cleanup` — remove stray `yarn.lock` @@ -205,10 +206,20 @@ If `package.json` does not exist: npm init -y ``` -If `composer.json` does not exist, create it using the plugin metadata: +If `composer.json` does not exist, create it as a JSON file using the plugin +metadata. Do **not** pass user-provided metadata to `composer init` on the +command line — write the file directly to avoid shell-quoting and injection +issues: -```sh -composer init --no-interaction --name=/ --description="{Description}" --license={License} +```json +{ + "name": "/", + "description": "{Description}", + "type": "wordpress-plugin", + "license": "{License}", + "require": {}, + "require-dev": {} +} ``` If `.git/` does not exist: @@ -261,7 +272,7 @@ Replace `` with the actual plugin slug (folder name / text domain). { "scripts": { "test": "phpunit", - "lint": "phpcs --standard=WordPress --extensions=php .", + "lint": "phpcs", "check": "wp plugin check --format=text" } } @@ -269,9 +280,18 @@ Replace `` with the actual plugin slug (folder name / text domain). > **Note:** The `check` script requires [Plugin Check (PCP)](https://wordpress.org/plugins/plugin-check/) installed and activated in WordPress, and WP-CLI available. Install with `wp plugin install plugin-check --activate`. +**PHP lint ruleset.** Create `phpcs.xml` so the `lint` script (`phpcs`) uses a +committed WordPress ruleset and excludes `vendor/` and `node_modules/`. Skip if +`phpcs.xml` (or `phpcs.xml.dist`) already exists. See: `references/linting-setup.md`. + +**PHP test scaffolding.** Skip any file that already exists. Create +`phpunit.xml.dist`, `tests/bootstrap.php`, and `tests/TestCase.php` (wires Brain +Monkey `setUp`/`tearDown`) so `composer test` runs out of the box. See: +`references/php-testing.md`. + See: `references/composer-setup.md` -Completion criterion: Composer dev dependencies are installed and `scripts.test`, `scripts.lint`, and `scripts.check` exist without overwriting unrelated scripts. +Completion criterion: Composer dev dependencies are installed; `scripts.test`, `scripts.lint`, and `scripts.check` exist without overwriting unrelated scripts; `phpcs.xml` and the PHPUnit test scaffolding exist (or were already present). ### 4) Config files @@ -285,6 +305,31 @@ See: `references/config-files.md` Completion criterion: `.editorconfig` and `.gitignore` are present, and existing `.gitignore` entries were merged non-destructively. +### 4b) ESLint (JavaScript linting) + +**Skip if an ESLint config (`.eslintrc.json`, `.eslintrc.js`, or `eslint.config.js`) already exists.** + +Install the latest releases and use WordPress' shared config: + +```sh +npm install --save-dev eslint @wordpress/eslint-plugin +``` + +Create `.eslintrc.json` and `.eslintignore`, then merge a `lint:js` script into +`package.json`: + +```json +{ + "scripts": { + "lint:js": "eslint ." + } +} +``` + +See: `references/linting-setup.md` + +Completion criterion: `.eslintrc.json`, `.eslintignore`, and `scripts.lint:js` are present, or a skip reason is recorded. + ### 5) Vitest setup **Skip if `vitest.config.js` already exists.** @@ -379,6 +424,8 @@ Completion criterion: Final report includes per-phase status, skipped reasons, a - `composer validate` passes. - `npm ls` shows no missing peer dependencies for vitest. - Agent skills are present under `~/.copilot/skills/` or `~/.agents/skills/`. +- `phpcs.xml`, `phpunit.xml.dist`, and `tests/` scaffolding exist when the `composer` phase ran. +- `.eslintrc.json` and `scripts.lint:js` exist when the `eslint` phase ran. - `.github/instructions/wordpress.instructions.md` exists when the `instructions` phase ran. ## Failure modes / debugging diff --git a/plugins/wordpress-skills/skills/prepare-wordpress/references/composer-setup.md b/plugins/wordpress-skills/skills/prepare-wordpress/references/composer-setup.md index 6dd127e..e97911d 100644 --- a/plugins/wordpress-skills/skills/prepare-wordpress/references/composer-setup.md +++ b/plugins/wordpress-skills/skills/prepare-wordpress/references/composer-setup.md @@ -34,7 +34,7 @@ Replace `` with the actual plugin slug (folder name / text domain). { "scripts": { "test": "phpunit", - "lint": "phpcs --standard=WordPress --extensions=php .", + "lint": "phpcs", "check": "wp plugin check --format=text" } } @@ -42,6 +42,11 @@ Replace `` with the actual plugin slug (folder name / text domain). > **Note:** The `check` script requires [Plugin Check (PCP)](https://wordpress.org/plugins/plugin-check/) to be installed and activated in the WordPress environment, and WP-CLI to be available. Install it with `wp plugin install plugin-check --activate`. +> **`lint` uses `phpcs.xml`:** The `lint` script runs plain `phpcs`, which reads +> the committed `phpcs.xml` ruleset. Create it (and the ESLint config) per +> `linting-setup.md`. Scaffold PHPUnit test files per `php-testing.md` so +> `composer test` runs out of the box. + ### How to merge via PHP (if jq is unavailable) ```sh @@ -50,7 +55,7 @@ $f = "composer.json"; $c = json_decode(file_get_contents($f), true); $c["scripts"] = array_merge($c["scripts"] ?? [], [ "test" => "phpunit", - "lint" => "phpcs --standard=WordPress --extensions=php .", + "lint" => "phpcs", "check" => "wp plugin check --format=text" ]); file_put_contents($f, json_encode($c, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"); diff --git a/plugins/wordpress-skills/skills/prepare-wordpress/references/linting-setup.md b/plugins/wordpress-skills/skills/prepare-wordpress/references/linting-setup.md new file mode 100644 index 0000000..0b91150 --- /dev/null +++ b/plugins/wordpress-skills/skills/prepare-wordpress/references/linting-setup.md @@ -0,0 +1,76 @@ +# Linting Setup + +Two linters: **PHPCS** (WordPress Coding Standards) for PHP and **ESLint** +(`@wordpress/eslint-plugin`) for JavaScript. Do not pin versions — install the +latest compatible releases. + +## PHP: phpcs.xml + +Committing a `phpcs.xml` ruleset lets the `lint` composer script run as plain +`phpcs` (no `--standard` flag) and excludes build/vendor paths. Requires +`wp-coding-standards/wpcs` and `dealerdirect/phpcodesniffer-composer-installer` +(installed in the Composer phase). + +Create `phpcs.xml` at the project root. Skip if it already exists. + +```xml + + + WordPress Coding Standards for this project. + + . + vendor/* + node_modules/* + build/* + + + + + + +``` + +Run with `composer lint` (i.e. `phpcs`) and auto-fix with `phpcbf`. + +## JavaScript: ESLint + +Install: + +```sh +npm install --save-dev eslint @wordpress/eslint-plugin +``` + +Create `.eslintrc.json`: + +```json +{ + "root": true, + "extends": ["plugin:@wordpress/eslint-plugin/recommended"] +} +``` + +Create `.eslintignore`: + +```gitignore +vendor/ +node_modules/ +build/ +``` + +Merge into `package.json` scripts (do not overwrite existing): + +```json +{ + "scripts": { + "lint:js": "eslint ." + } +} +``` + +Run with `npm run lint:js`. Auto-fix with `eslint . --fix`. + +## Notes + +- Skip PHP or JS linting independently if a config already exists. +- `@wordpress/eslint-plugin/recommended` covers React/JSX and WordPress globals; + use `.../recommended-with-formatting` if you are not running Prettier. diff --git a/plugins/wordpress-skills/skills/prepare-wordpress/scripts/detect_project.mjs b/plugins/wordpress-skills/skills/prepare-wordpress/scripts/detect_project.mjs index 5396216..fd23cb3 100644 --- a/plugins/wordpress-skills/skills/prepare-wordpress/scripts/detect_project.mjs +++ b/plugins/wordpress-skills/skills/prepare-wordpress/scripts/detect_project.mjs @@ -133,6 +133,10 @@ export function detectProjectState(targetRoot = process.cwd()) { check: composerHasScript("check"), }, + // PHP lint/test config + phpcsXml: exists("phpcs.xml") || exists("phpcs.xml.dist"), + phpunitConfig: exists("phpunit.xml.dist") || exists("phpunit.xml"), + // Config files editorconfig: exists(".editorconfig"), gitignore: exists(".gitignore"), @@ -145,6 +149,13 @@ export function detectProjectState(targetRoot = process.cwd()) { testScript: packageJsonHasScript("test:js"), }, + // ESLint + eslint: { + devDep: packageJsonHasDevDep("eslint"), + config: exists(".eslintrc.json") || exists(".eslintrc.js") || exists("eslint.config.js") || exists(".eslintrc"), + script: packageJsonHasScript("lint:js"), + }, + // i18n i18n: { mapJson: exists("i18n-map.json"), @@ -197,6 +208,11 @@ export function buildDetectionSummary(state, repoRoot = process.cwd()) { const missingComposerScripts = Object.entries(state.composerScripts).filter(([, v]) => !v).map(([k]) => k); if (missingComposerScripts.length > 0) lines.push(`📦 Composer scripts to add: ${missingComposerScripts.join(", ")}`); + if (!state.phpcsXml) lines.push("📦 phpcs.xml — will create"); + else lines.push("⏭ phpcs.xml exists"); + if (!state.phpunitConfig) lines.push("📦 PHPUnit test scaffolding — will create"); + else lines.push("⏭ PHPUnit config exists"); + // Config files if (!state.editorconfig) lines.push("📦 .editorconfig — will create"); else lines.push("⏭ .editorconfig exists"); @@ -208,6 +224,10 @@ export function buildDetectionSummary(state, repoRoot = process.cwd()) { if (!state.vitest.config) lines.push("\n📦 Vitest — will install and configure"); else lines.push("\n⏭ Vitest already configured"); + // ESLint + if (!state.eslint.config) lines.push("📦 ESLint — will install and configure"); + else lines.push("⏭ ESLint already configured"); + // i18n if (!state.i18n.mapJson || !state.i18n.npmScripts) lines.push("📦 i18n — will scaffold"); else lines.push("⏭ i18n already configured"); diff --git a/plugins/wordpress-skills/skills/prepare-wordpress/scripts/plan_setup.mjs b/plugins/wordpress-skills/skills/prepare-wordpress/scripts/plan_setup.mjs index ec2289b..6183070 100644 --- a/plugins/wordpress-skills/skills/prepare-wordpress/scripts/plan_setup.mjs +++ b/plugins/wordpress-skills/skills/prepare-wordpress/scripts/plan_setup.mjs @@ -1,7 +1,7 @@ import { execSync } from "node:child_process"; import { detectProjectState } from "./detect_project.mjs"; -const ALL_PHASES = ["plugin", "readme", "init", "skills", "composer", "config", "vitest", "i18n", "instructions", "cleanup"]; +const ALL_PHASES = ["plugin", "readme", "init", "skills", "composer", "config", "vitest", "eslint", "i18n", "instructions", "cleanup"]; function parseArgs(argv) { const opts = { @@ -96,6 +96,8 @@ function buildPlan(state, phases) { if (!state.composerScripts.test || !state.composerScripts.lint || !state.composerScripts.check) { composerNotes.push("Manual: merge composer scripts test/lint/check into composer.json without overwriting existing scripts."); } + if (!state.phpcsXml) composerNotes.push("Manual: create phpcs.xml from references/linting-setup.md."); + if (!state.phpunitConfig) composerNotes.push("Manual: create phpunit.xml.dist, tests/bootstrap.php, and tests/TestCase.php from references/php-testing.md."); add("composer", composerCommands.length > 0 || composerNotes.length > 0 ? "Configure Composer tooling" : "Composer phase already satisfied", composerCommands, composerNotes); const configNotes = []; @@ -114,6 +116,13 @@ function buildPlan(state, phases) { } add("vitest", vitestCommands.length > 0 || vitestNotes.length > 0 ? "Configure Vitest" : "Vitest phase already satisfied", vitestCommands, vitestNotes); + const eslintCommands = []; + const eslintNotes = []; + if (!state.eslint.config || !state.eslint.devDep) eslintCommands.push("npm install --save-dev eslint @wordpress/eslint-plugin"); + if (!state.eslint.config) eslintNotes.push("Manual: create .eslintrc.json and .eslintignore from references/linting-setup.md."); + if (!state.eslint.script) eslintNotes.push("Manual: merge lint:js script into package.json without overwriting existing scripts."); + add("eslint", eslintCommands.length > 0 || eslintNotes.length > 0 ? "Configure ESLint" : "ESLint phase already satisfied", eslintCommands, eslintNotes); + const i18nNotes = []; if (!state.i18n.mapJson) i18nNotes.push("Manual: create i18n-map.json (or empty {} placeholder) from references/i18n-setup.md."); if (!state.i18n.languagesDir) i18nNotes.push("Manual: create languages/ directory."); diff --git a/scripts/build-agent-plugin.mjs b/scripts/build-agent-plugin.mjs index a5fb6dd..c48ef69 100644 --- a/scripts/build-agent-plugin.mjs +++ b/scripts/build-agent-plugin.mjs @@ -24,7 +24,7 @@ const PLUGINS = [ distRepo: 'https://github.com/soderlind/wordpress-agent-plugin', manifest: { name: 'wordpress-skills', - version: '1.0.0', + version: '1.1.0', description: 'WordPress development, testing, and release skills.', author: { name: 'Per Soderlind', url: 'https://soderlind.no' }, homepage: 'https://github.com/soderlind/wordpress-agent-plugin', @@ -34,6 +34,19 @@ const PLUGINS = [ }, // Newest first. Each entry becomes a section in CHANGELOG.md. changelog: [ + { + version: '1.1.0', + date: '2026-08-08', + added: [ + 'prepare-wordpress: unit tests mock WordPress with Brain Monkey (brain/monkey); added references/php-testing.md and PHPUnit test scaffolding (phpunit.xml.dist, tests/bootstrap.php, tests/TestCase.php).', + 'prepare-wordpress: ESLint (@wordpress/eslint-plugin) and a committed phpcs.xml ruleset via a new linting-setup.md reference.', + 'prepare-wordpress: new instructions phase downloads the github/awesome-copilot WordPress coding instructions.', + ], + changed: [ + 'prepare-wordpress: Composer and npm dependencies install the latest compatible releases (unpinned).', + 'prepare-wordpress: composer.json is written as a file instead of via composer init with user-provided strings.', + ], + }, { version: '1.0.0', date: '2026-08-07', diff --git a/skills/prepare-wordpress/SKILL.md b/skills/prepare-wordpress/SKILL.md index 1af34c5..a1bb011 100644 --- a/skills/prepare-wordpress/SKILL.md +++ b/skills/prepare-wordpress/SKILL.md @@ -2,7 +2,7 @@ name: prepare-wordpress description: "Phase-based WordPress project setup workflow with dry-run planning and confirmed apply for predictable scaffolding/standardization." compatibility: "macOS/Linux with Node.js 18+, Composer 2+, PHP 8.3+, git. Optional: WP-CLI for i18n commands, curl for downloading coding instructions." -version: "1.2.0" +version: "1.3.0" --- # Prepare WordPress Project @@ -61,6 +61,7 @@ Hard gate: Do not run any `--apply` command until dry-run output is shown and th - `composer` — install PHP dev deps and merge scripts - `config` — create/merge `.editorconfig` and `.gitignore` - `vitest` — install and scaffold Vitest (JavaScript test runner) +- `eslint` — install and configure ESLint for JavaScript - `i18n` — scaffold i18n files/scripts - `instructions` — download WordPress Copilot coding instructions - `cleanup` — remove stray `yarn.lock` @@ -205,10 +206,20 @@ If `package.json` does not exist: npm init -y ``` -If `composer.json` does not exist, create it using the plugin metadata: +If `composer.json` does not exist, create it as a JSON file using the plugin +metadata. Do **not** pass user-provided metadata to `composer init` on the +command line — write the file directly to avoid shell-quoting and injection +issues: -```sh -composer init --no-interaction --name=/ --description="{Description}" --license={License} +```json +{ + "name": "/", + "description": "{Description}", + "type": "wordpress-plugin", + "license": "{License}", + "require": {}, + "require-dev": {} +} ``` If `.git/` does not exist: @@ -261,7 +272,7 @@ Replace `` with the actual plugin slug (folder name / text domain). { "scripts": { "test": "phpunit", - "lint": "phpcs --standard=WordPress --extensions=php .", + "lint": "phpcs", "check": "wp plugin check --format=text" } } @@ -269,9 +280,18 @@ Replace `` with the actual plugin slug (folder name / text domain). > **Note:** The `check` script requires [Plugin Check (PCP)](https://wordpress.org/plugins/plugin-check/) installed and activated in WordPress, and WP-CLI available. Install with `wp plugin install plugin-check --activate`. +**PHP lint ruleset.** Create `phpcs.xml` so the `lint` script (`phpcs`) uses a +committed WordPress ruleset and excludes `vendor/` and `node_modules/`. Skip if +`phpcs.xml` (or `phpcs.xml.dist`) already exists. See: `references/linting-setup.md`. + +**PHP test scaffolding.** Skip any file that already exists. Create +`phpunit.xml.dist`, `tests/bootstrap.php`, and `tests/TestCase.php` (wires Brain +Monkey `setUp`/`tearDown`) so `composer test` runs out of the box. See: +`references/php-testing.md`. + See: `references/composer-setup.md` -Completion criterion: Composer dev dependencies are installed and `scripts.test`, `scripts.lint`, and `scripts.check` exist without overwriting unrelated scripts. +Completion criterion: Composer dev dependencies are installed; `scripts.test`, `scripts.lint`, and `scripts.check` exist without overwriting unrelated scripts; `phpcs.xml` and the PHPUnit test scaffolding exist (or were already present). ### 4) Config files @@ -285,6 +305,31 @@ See: `references/config-files.md` Completion criterion: `.editorconfig` and `.gitignore` are present, and existing `.gitignore` entries were merged non-destructively. +### 4b) ESLint (JavaScript linting) + +**Skip if an ESLint config (`.eslintrc.json`, `.eslintrc.js`, or `eslint.config.js`) already exists.** + +Install the latest releases and use WordPress' shared config: + +```sh +npm install --save-dev eslint @wordpress/eslint-plugin +``` + +Create `.eslintrc.json` and `.eslintignore`, then merge a `lint:js` script into +`package.json`: + +```json +{ + "scripts": { + "lint:js": "eslint ." + } +} +``` + +See: `references/linting-setup.md` + +Completion criterion: `.eslintrc.json`, `.eslintignore`, and `scripts.lint:js` are present, or a skip reason is recorded. + ### 5) Vitest setup **Skip if `vitest.config.js` already exists.** @@ -379,6 +424,8 @@ Completion criterion: Final report includes per-phase status, skipped reasons, a - `composer validate` passes. - `npm ls` shows no missing peer dependencies for vitest. - Agent skills are present under `~/.copilot/skills/` or `~/.agents/skills/`. +- `phpcs.xml`, `phpunit.xml.dist`, and `tests/` scaffolding exist when the `composer` phase ran. +- `.eslintrc.json` and `scripts.lint:js` exist when the `eslint` phase ran. - `.github/instructions/wordpress.instructions.md` exists when the `instructions` phase ran. ## Failure modes / debugging diff --git a/skills/prepare-wordpress/references/composer-setup.md b/skills/prepare-wordpress/references/composer-setup.md index 6dd127e..e97911d 100644 --- a/skills/prepare-wordpress/references/composer-setup.md +++ b/skills/prepare-wordpress/references/composer-setup.md @@ -34,7 +34,7 @@ Replace `` with the actual plugin slug (folder name / text domain). { "scripts": { "test": "phpunit", - "lint": "phpcs --standard=WordPress --extensions=php .", + "lint": "phpcs", "check": "wp plugin check --format=text" } } @@ -42,6 +42,11 @@ Replace `` with the actual plugin slug (folder name / text domain). > **Note:** The `check` script requires [Plugin Check (PCP)](https://wordpress.org/plugins/plugin-check/) to be installed and activated in the WordPress environment, and WP-CLI to be available. Install it with `wp plugin install plugin-check --activate`. +> **`lint` uses `phpcs.xml`:** The `lint` script runs plain `phpcs`, which reads +> the committed `phpcs.xml` ruleset. Create it (and the ESLint config) per +> `linting-setup.md`. Scaffold PHPUnit test files per `php-testing.md` so +> `composer test` runs out of the box. + ### How to merge via PHP (if jq is unavailable) ```sh @@ -50,7 +55,7 @@ $f = "composer.json"; $c = json_decode(file_get_contents($f), true); $c["scripts"] = array_merge($c["scripts"] ?? [], [ "test" => "phpunit", - "lint" => "phpcs --standard=WordPress --extensions=php .", + "lint" => "phpcs", "check" => "wp plugin check --format=text" ]); file_put_contents($f, json_encode($c, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"); diff --git a/skills/prepare-wordpress/references/linting-setup.md b/skills/prepare-wordpress/references/linting-setup.md new file mode 100644 index 0000000..0b91150 --- /dev/null +++ b/skills/prepare-wordpress/references/linting-setup.md @@ -0,0 +1,76 @@ +# Linting Setup + +Two linters: **PHPCS** (WordPress Coding Standards) for PHP and **ESLint** +(`@wordpress/eslint-plugin`) for JavaScript. Do not pin versions — install the +latest compatible releases. + +## PHP: phpcs.xml + +Committing a `phpcs.xml` ruleset lets the `lint` composer script run as plain +`phpcs` (no `--standard` flag) and excludes build/vendor paths. Requires +`wp-coding-standards/wpcs` and `dealerdirect/phpcodesniffer-composer-installer` +(installed in the Composer phase). + +Create `phpcs.xml` at the project root. Skip if it already exists. + +```xml + + + WordPress Coding Standards for this project. + + . + vendor/* + node_modules/* + build/* + + + + + + +``` + +Run with `composer lint` (i.e. `phpcs`) and auto-fix with `phpcbf`. + +## JavaScript: ESLint + +Install: + +```sh +npm install --save-dev eslint @wordpress/eslint-plugin +``` + +Create `.eslintrc.json`: + +```json +{ + "root": true, + "extends": ["plugin:@wordpress/eslint-plugin/recommended"] +} +``` + +Create `.eslintignore`: + +```gitignore +vendor/ +node_modules/ +build/ +``` + +Merge into `package.json` scripts (do not overwrite existing): + +```json +{ + "scripts": { + "lint:js": "eslint ." + } +} +``` + +Run with `npm run lint:js`. Auto-fix with `eslint . --fix`. + +## Notes + +- Skip PHP or JS linting independently if a config already exists. +- `@wordpress/eslint-plugin/recommended` covers React/JSX and WordPress globals; + use `.../recommended-with-formatting` if you are not running Prettier. diff --git a/skills/prepare-wordpress/scripts/detect_project.mjs b/skills/prepare-wordpress/scripts/detect_project.mjs index 5396216..fd23cb3 100644 --- a/skills/prepare-wordpress/scripts/detect_project.mjs +++ b/skills/prepare-wordpress/scripts/detect_project.mjs @@ -133,6 +133,10 @@ export function detectProjectState(targetRoot = process.cwd()) { check: composerHasScript("check"), }, + // PHP lint/test config + phpcsXml: exists("phpcs.xml") || exists("phpcs.xml.dist"), + phpunitConfig: exists("phpunit.xml.dist") || exists("phpunit.xml"), + // Config files editorconfig: exists(".editorconfig"), gitignore: exists(".gitignore"), @@ -145,6 +149,13 @@ export function detectProjectState(targetRoot = process.cwd()) { testScript: packageJsonHasScript("test:js"), }, + // ESLint + eslint: { + devDep: packageJsonHasDevDep("eslint"), + config: exists(".eslintrc.json") || exists(".eslintrc.js") || exists("eslint.config.js") || exists(".eslintrc"), + script: packageJsonHasScript("lint:js"), + }, + // i18n i18n: { mapJson: exists("i18n-map.json"), @@ -197,6 +208,11 @@ export function buildDetectionSummary(state, repoRoot = process.cwd()) { const missingComposerScripts = Object.entries(state.composerScripts).filter(([, v]) => !v).map(([k]) => k); if (missingComposerScripts.length > 0) lines.push(`📦 Composer scripts to add: ${missingComposerScripts.join(", ")}`); + if (!state.phpcsXml) lines.push("📦 phpcs.xml — will create"); + else lines.push("⏭ phpcs.xml exists"); + if (!state.phpunitConfig) lines.push("📦 PHPUnit test scaffolding — will create"); + else lines.push("⏭ PHPUnit config exists"); + // Config files if (!state.editorconfig) lines.push("📦 .editorconfig — will create"); else lines.push("⏭ .editorconfig exists"); @@ -208,6 +224,10 @@ export function buildDetectionSummary(state, repoRoot = process.cwd()) { if (!state.vitest.config) lines.push("\n📦 Vitest — will install and configure"); else lines.push("\n⏭ Vitest already configured"); + // ESLint + if (!state.eslint.config) lines.push("📦 ESLint — will install and configure"); + else lines.push("⏭ ESLint already configured"); + // i18n if (!state.i18n.mapJson || !state.i18n.npmScripts) lines.push("📦 i18n — will scaffold"); else lines.push("⏭ i18n already configured"); diff --git a/skills/prepare-wordpress/scripts/plan_setup.mjs b/skills/prepare-wordpress/scripts/plan_setup.mjs index ec2289b..6183070 100644 --- a/skills/prepare-wordpress/scripts/plan_setup.mjs +++ b/skills/prepare-wordpress/scripts/plan_setup.mjs @@ -1,7 +1,7 @@ import { execSync } from "node:child_process"; import { detectProjectState } from "./detect_project.mjs"; -const ALL_PHASES = ["plugin", "readme", "init", "skills", "composer", "config", "vitest", "i18n", "instructions", "cleanup"]; +const ALL_PHASES = ["plugin", "readme", "init", "skills", "composer", "config", "vitest", "eslint", "i18n", "instructions", "cleanup"]; function parseArgs(argv) { const opts = { @@ -96,6 +96,8 @@ function buildPlan(state, phases) { if (!state.composerScripts.test || !state.composerScripts.lint || !state.composerScripts.check) { composerNotes.push("Manual: merge composer scripts test/lint/check into composer.json without overwriting existing scripts."); } + if (!state.phpcsXml) composerNotes.push("Manual: create phpcs.xml from references/linting-setup.md."); + if (!state.phpunitConfig) composerNotes.push("Manual: create phpunit.xml.dist, tests/bootstrap.php, and tests/TestCase.php from references/php-testing.md."); add("composer", composerCommands.length > 0 || composerNotes.length > 0 ? "Configure Composer tooling" : "Composer phase already satisfied", composerCommands, composerNotes); const configNotes = []; @@ -114,6 +116,13 @@ function buildPlan(state, phases) { } add("vitest", vitestCommands.length > 0 || vitestNotes.length > 0 ? "Configure Vitest" : "Vitest phase already satisfied", vitestCommands, vitestNotes); + const eslintCommands = []; + const eslintNotes = []; + if (!state.eslint.config || !state.eslint.devDep) eslintCommands.push("npm install --save-dev eslint @wordpress/eslint-plugin"); + if (!state.eslint.config) eslintNotes.push("Manual: create .eslintrc.json and .eslintignore from references/linting-setup.md."); + if (!state.eslint.script) eslintNotes.push("Manual: merge lint:js script into package.json without overwriting existing scripts."); + add("eslint", eslintCommands.length > 0 || eslintNotes.length > 0 ? "Configure ESLint" : "ESLint phase already satisfied", eslintCommands, eslintNotes); + const i18nNotes = []; if (!state.i18n.mapJson) i18nNotes.push("Manual: create i18n-map.json (or empty {} placeholder) from references/i18n-setup.md."); if (!state.i18n.languagesDir) i18nNotes.push("Manual: create languages/ directory.");