Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .well-known/agent-skills/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:9184137e00076315359b4298eeb577beee1dd7353976d1fc463443e565e51b66"
"digest": "sha256:28fd8747a249eddb8308c4d3eb5bfe730db6a3470ab169092fe506b6a1fd26f2"
},
{
"name": "wp-bump",
Expand Down
Binary file modified .well-known/agent-skills/prepare-wordpress.tar.gz
Binary file not shown.
38 changes: 34 additions & 4 deletions plugins/wordpress-skills/skills/prepare-wordpress/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
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."
version: "1.1.0"
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"
---

# Prepare WordPress Project
Expand Down Expand Up @@ -60,8 +60,9 @@ Hard gate: Do not run any `--apply` command until dry-run output is shown and th
- `skills` — install agent skills
- `composer` — install PHP dev deps and merge scripts
- `config` — create/merge `.editorconfig` and `.gitignore`
- `vitest` — install and scaffold Vitest
- `vitest` — install and scaffold Vitest (JavaScript test runner)
- `i18n` — scaffold i18n files/scripts
- `instructions` — download WordPress Copilot coding instructions
- `cleanup` — remove stray `yarn.lock`

Use the planner script to preview actions:
Expand Down Expand Up @@ -245,9 +246,13 @@ Completion criterion: Each listed skill is installed or explicitly skipped becau
Install all PHP dev dependencies in a single command:

```sh
composer require --dev phpunit/phpunit wp-coding-standards/wpcs dealerdirect/phpcodesniffer-composer-installer pestphp/pest
composer require --dev phpunit/phpunit brain/monkey wp-coding-standards/wpcs dealerdirect/phpcodesniffer-composer-installer pestphp/pest
```

> **Dependencies:** Do not pin versions — let Composer resolve the latest compatible releases. Run `composer update` periodically to stay current.

Unit tests mock WordPress functions with **Brain Monkey** (`brain/monkey`, which pulls in `mockery/mockery`), so tests run without a full WordPress install. See: `references/php-testing.md`.

Then merge these scripts into `composer.json` (do not overwrite existing scripts).

Replace `<plugin-slug>` with the actual plugin slug (folder name / text domain).
Expand Down Expand Up @@ -284,6 +289,9 @@ Completion criterion: `.editorconfig` and `.gitignore` are present, and existing

**Skip if `vitest.config.js` already exists.**

Vitest is the JavaScript test runner for this project. Install the latest
releases (no pinned versions):

```sh
npm install --save-dev vitest jsdom
```
Expand Down Expand Up @@ -323,6 +331,27 @@ See: `references/i18n-setup.md`

Completion criterion: `i18n-map.json`, `languages/`, and i18n npm scripts are present (or explicit skip/defer rationale is recorded).

### 6b) WordPress Copilot coding instructions

**Skip if `.github/instructions/wordpress.instructions.md` already exists.**

Add the community WordPress coding, security, and testing rules from
[github/awesome-copilot](https://github.com/github/awesome-copilot/blob/main/instructions/wordpress.instructions.md)
so agents follow WordPress best practices in this project:

```sh
mkdir -p .github/instructions
curl -fsSL https://raw.githubusercontent.com/github/awesome-copilot/main/instructions/wordpress.instructions.md \
-o .github/instructions/wordpress.instructions.md
```

If `curl` is unavailable or you are offline, create the file manually from
`references/copilot-instructions.md`.

See: `references/copilot-instructions.md`

Completion criterion: `.github/instructions/wordpress.instructions.md` exists (downloaded or created manually), or a skip reason is recorded.

### 7) Cleanup

Remove any stray `yarn.lock` file that may have been created by `npx` commands:
Expand Down Expand Up @@ -350,6 +379,7 @@ 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/`.
- `.github/instructions/wordpress.instructions.md` exists when the `instructions` phase ran.

## Failure modes / debugging

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,28 @@

## Dependencies

Install all PHP dev dependencies in one command:
Install all PHP dev dependencies in one command (no pinned versions — Composer
resolves the latest compatible releases):

```sh
composer require --dev phpunit/phpunit wp-coding-standards/wpcs dealerdirect/phpcodesniffer-composer-installer pestphp/pest
composer require --dev phpunit/phpunit brain/monkey wp-coding-standards/wpcs dealerdirect/phpcodesniffer-composer-installer pestphp/pest
```

### What each package does

| Package | Purpose |
| --- | --- |
| `phpunit/phpunit` | PHP unit testing framework |
| `brain/monkey` | Mocks WordPress core functions/hooks in unit tests (pulls in `mockery/mockery`) so tests run without a WordPress install |
| `wp-coding-standards/wpcs` | WordPress PHP Coding Standards ruleset for PHP_CodeSniffer |
| `dealerdirect/phpcodesniffer-composer-installer` | Auto-registers PHPCS standards (including WPCS) with Composer |
| `pestphp/pest` | Elegant PHP testing framework built on PHPUnit |

> **Latest versions:** Do not pin versions in `composer require`. Run
> `composer update` periodically to pick up new compatible releases.

For how to write unit tests with Brain Monkey, see `php-testing.md`.

## Scripts

Merge these scripts into `composer.json`. Do NOT overwrite existing scripts — only add missing keys.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# WordPress Copilot Coding Instructions

This phase adds a project-scoped Copilot instructions file so agents follow
WordPress coding, security, i18n, performance, and testing best practices.

Source: [github/awesome-copilot](https://github.com/github/awesome-copilot/blob/main/instructions/wordpress.instructions.md)
(MIT License). Prefer downloading the canonical file so it stays current.

## Preferred: download the latest

```sh
mkdir -p .github/instructions
curl -fsSL https://raw.githubusercontent.com/github/awesome-copilot/main/instructions/wordpress.instructions.md \
-o .github/instructions/wordpress.instructions.md
```

The `applyTo` frontmatter in that file scopes the rules to WordPress plugin,
theme, PHP, JS, and CSS paths automatically.

## Fallback: create manually (offline)

If `curl` is unavailable, create `.github/instructions/wordpress.instructions.md`
with the content below. This is a condensed version adapted from
github/awesome-copilot (MIT); replace it with the canonical file when online.

````markdown
---
applyTo: 'wp-content/plugins/**,wp-content/themes/**,**/*.php,**/*.inc,**/*.js,**/*.jsx,**/*.ts,**/*.tsx,**/*.css,**/*.scss,**/*.json'
description: 'Coding, security, and testing rules for WordPress plugins and themes'
---

# WordPress Development — Copilot Instructions

Generate WordPress code that is secure, performant, testable, and compliant with
official WordPress practices. Prefer hooks, small functions, and clear separation
of concerns.

## Core principles
- Never modify WordPress core. Extend via actions and filters.
- Guard entry PHP files with `defined('ABSPATH') || exit;` and a plugin header.
- Use unique prefixes or PHP namespaces to avoid global collisions.
- Enqueue assets; never inline raw `<script>`/`<style>` in PHP templates.
- Make user-facing strings translatable with the correct text domain.

## Coding standards
- Follow WordPress Coding Standards (WPCS); write DocBlocks for public APIs.
- PHP: prefer strict comparisons (`===`, `!==`); target the project's PHP version.
- JS: match WordPress JS style; prefer `@wordpress/*` packages for block/editor code.
- Lint PHP with `phpcs`/`phpcbf` (WPCS) and JS with ESLint (`@wordpress/eslint-plugin`).

## Security & data handling
- Escape on output: `esc_html()`, `esc_attr()`, `esc_url()`, `wp_kses_post()`.
- Sanitize on input: `sanitize_text_field()`, `sanitize_email()`, `absint()`, etc.
- Verify nonces (`wp_verify_nonce()` / `check_admin_referer()`) and capabilities
(`current_user_can()`) for every write (forms, AJAX, REST).
- Database: always use `$wpdb->prepare()` with placeholders; never concatenate input.
- Uploads: validate type and use `wp_handle_upload()` / `media_handle_upload()`.

## Internationalization
- Wrap user-visible strings: `__()`, `_x()`, `esc_html__()` with the text domain.
- Load translations with `load_plugin_textdomain()` / `load_theme_textdomain()`.
- Keep a `.pot` in `/languages` and use the domain consistently.

## Performance
- Defer heavy logic to specific hooks; avoid expensive work on `init`.
- Use transients or object caching for expensive queries; plan invalidation.
- Enqueue only what you need, conditionally (front vs admin; specific screens).

## REST API
- Register with `register_rest_route()`; always set a `permission_callback`.
- Validate/sanitize args via the `args` schema; return `WP_REST_Response`.

## Blocks & editor
- Use `block.json` + `register_block_type()` and `@wordpress/*` packages.
- Provide server render callbacks for dynamic blocks.

## Testing
- PHP unit: mock WordPress with Brain Monkey (`brain/monkey`) + PHPUnit/Pest;
test sanitization, capability checks, REST permissions, and hooks.
- PHP integration: use the WordPress test suite (`WP_UnitTestCase`) with factories.
- JavaScript: use Vitest (`vitest run`).
- E2E: use Playwright for editor/front-end journeys.

## Checklist
- Unique prefixes/namespaces; no accidental globals.
- Nonce + capability checks for any write action.
- Inputs sanitized; outputs escaped.
- User-visible strings wrapped in i18n with the correct text domain.
- Assets enqueued via APIs (no inline script/style).
- Tests added/updated; code passes PHPCS (WPCS) and ESLint.
- Never concatenate untrusted input into SQL; always prepare queries.
````
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# PHP Unit Testing with Brain Monkey

Unit tests mock WordPress core functions and hooks with
[Brain Monkey](https://github.com/Brain-WP/BrainMonkey) so they run fast and
without a full WordPress install. Use this for pure PHP logic. For
integration-level tests that need a real database and WordPress runtime, use the
official WordPress test suite (`WP_UnitTestCase`) instead.

## Install

```sh
composer require --dev phpunit/phpunit brain/monkey
```

`brain/monkey` depends on `mockery/mockery` and `antecedent/patchwork`, which
Composer installs automatically. Do not pin versions — take the latest
compatible releases.

## phpunit.xml.dist

Create at project root:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/bootstrap.php" colors="true">
<testsuites>
<testsuite name="unit">
<directory suffix="Test.php">tests</directory>
</testsuite>
</testsuites>
</phpunit>
```

## tests/bootstrap.php

```php
<?php

declare(strict_types=1);

require_once dirname(__DIR__) . '/vendor/autoload.php';
```

## tests/TestCase.php

A shared base that wires Brain Monkey setup/teardown:

```php
<?php

declare(strict_types=1);

namespace Tests;

use Brain\Monkey;
use PHPUnit\Framework\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
protected function setUp(): void
{
parent::setUp();
Monkey\setUp();
}

protected function tearDown(): void
{
Monkey\tearDown();
parent::tearDown();
}
}
```

## Example test

Mock WordPress functions and assert hooks/filters without loading WordPress:

```php
<?php

declare(strict_types=1);

namespace Tests;

use Brain\Monkey\Functions;
use Brain\Monkey\Filters;

final class FormatTitleTest extends TestCase
{
public function test_it_escapes_and_filters_the_title(): void
{
// Stub WordPress functions the code under test calls.
Functions\when('esc_html')->returnArg();
Functions\expect('sanitize_text_field')
->once()
->with(' Hello ')
->andReturn('Hello');

$result = my_plugin_format_title(' Hello ');

self::assertSame('Hello', $result);
self::assertSame(1, Filters\applied('my_plugin_title'));
}
}
```

Key Brain Monkey helpers:

- `Functions\when('fn')->justReturn($v)` / `->returnArg()` — light stubs.
- `Functions\expect('fn')->once()->with(...)->andReturn(...)` — assert calls.
- `Actions\expectAdded('hook')` / `Filters\expectApplied('hook')` — assert hooks.
- `Actions\has('hook', $cb)` — assert a callback was registered.

## Pest variant

If using Pest, call the same lifecycle in `tests/Pest.php`:

```php
<?php

use Brain\Monkey;

uses()
->beforeEach(fn () => Monkey\setUp())
->afterEach(fn () => Monkey\tearDown())
->in('Unit');
```

## Run

```sh
composer test # phpunit
# or
./vendor/bin/pest
```
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# Vitest Setup

Vitest is the JavaScript test runner for the project.

## Installation

Install the latest releases (do not pin versions):

```sh
npm install --save-dev vitest jsdom
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export function detectProjectState(targetRoot = process.cwd()) {
// Composer packages
composer: {
phpunit: composerHasPackage("phpunit/phpunit"),
brainMonkey: composerHasPackage("brain/monkey"),
wpcs: composerHasPackage("wp-coding-standards/wpcs"),
phpcsInstaller: composerHasPackage("dealerdirect/phpcodesniffer-composer-installer"),
pest: composerHasPackage("pestphp/pest"),
Expand Down Expand Up @@ -150,6 +151,9 @@ export function detectProjectState(targetRoot = process.cwd()) {
languagesDir: exists("languages"),
npmScripts: packageJsonHasScript("i18n"),
},

// Copilot coding instructions
instructionsFile: exists(".github/instructions/wordpress.instructions.md"),
};
}

Expand Down Expand Up @@ -208,6 +212,10 @@ export function buildDetectionSummary(state, repoRoot = process.cwd()) {
if (!state.i18n.mapJson || !state.i18n.npmScripts) lines.push("📦 i18n — will scaffold");
else lines.push("⏭ i18n already configured");

// Copilot instructions
if (!state.instructionsFile) lines.push("📦 Copilot instructions — will download wordpress.instructions.md");
else lines.push("⏭ Copilot instructions already present");

lines.push("");
return lines.join("\n");
}
Expand Down
Loading
Loading