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 43d8ceb..12f6662 100644 Binary files a/.well-known/agent-skills/prepare-wordpress.tar.gz and b/.well-known/agent-skills/prepare-wordpress.tar.gz differ 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.");