From a97812f073e31d8107334a8a1ad7364a76ee8e6e Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Thu, 29 Oct 2026 14:07:29 +0100 Subject: [PATCH 1/9] Keep a partial deploy inside the plugin's build-error paths Three paths of a partial deploy escaped the catches the template documents. uninstall.php called the framework's Uninstaller after its try, so a build missing the engine fataled the uninstall request. dws_tpl_deactivate() built the composition root without the catch dws_tpl_boot() has, so a deactivation on such a build fataled too. functions.php added its hooks before the includes loop, so an include that threw still left the boot and deactivation hooked to a plugin that had not loaded. uninstall.php checks for the engine inside its try, the deactivation reports a build error as the boot does, and the hooks are added after the includes. Rows run each case in its own request, as a missing blocks manifest already gated, which no row covered. Assisted-by: Claude Code:claude-opus-5-5 --- functions.php | 17 +++-- tests/Integration/PartialDeployTest.php | 87 +++++++++++++++++++++++++ tests/README.md | 2 +- uninstall.php | 3 + 4 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 tests/Integration/PartialDeployTest.php diff --git a/functions.php b/functions.php index 02a16f6..029a5dd 100644 --- a/functions.php +++ b/functions.php @@ -13,10 +13,6 @@ defined( 'ABSPATH' ) || exit; -add_action( 'plugins_loaded', 'dws_tpl_boot', 15 ); -add_action( 'before_woocommerce_init', 'dws_tpl_declare_compatibility' ); -register_deactivation_hook( DWS_TPL_FILE, 'dws_tpl_deactivate' ); - $dws_tpl_includes = glob( __DIR__ . '/includes/*.php' ); if ( false === $dws_tpl_includes ) { throw new RuntimeException( 'The includes directory could not be read.' ); @@ -29,6 +25,11 @@ } } +// The hooks come last, so an include that throws leaves nothing of the plugin hooked. +add_action( 'plugins_loaded', 'dws_tpl_boot', 15 ); +add_action( 'before_woocommerce_init', 'dws_tpl_declare_compatibility' ); +register_deactivation_hook( DWS_TPL_FILE, 'dws_tpl_deactivate' ); + /** * Returns the plugin's composition root, built on first use. * @@ -63,7 +64,7 @@ function dws_tpl_boot(): void { } /** - * Unschedules the plugin's jobs on the site it is deactivated on. + * Unschedules the plugin's jobs on the site it is deactivated on, reporting a composition root that cannot be built as an incomplete build. * * @since 2.0.0 * @version 2.0.0 @@ -71,7 +72,11 @@ function dws_tpl_boot(): void { * @return void */ function dws_tpl_deactivate(): void { - dws_tpl_plugin()->deactivate(); + try { + dws_tpl_plugin()->deactivate(); + } catch ( Throwable $error ) { + dws_tpl_handle_build_error( $error ); + } } /** diff --git a/tests/Integration/PartialDeployTest.php b/tests/Integration/PartialDeployTest.php new file mode 100644 index 0000000..cfd5b2a --- /dev/null +++ b/tests/Integration/PartialDeployTest.php @@ -0,0 +1,87 @@ +&1', $output, $status ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec -- Each row needs a fresh request that loads the plugin. + $output = \array_values( \array_filter( $output, static fn ( string $line ): bool => '' !== \trim( $line ) ) ); + + return array( + 'status' => $status, + 'output' => \implode( "\n", $output ), + 'last' => (string) \end( $output ), + ); + } +} diff --git a/tests/README.md b/tests/README.md index 9093fa8..0fcfda5 100644 --- a/tests/README.md +++ b/tests/README.md @@ -7,7 +7,7 @@ This document describes the plugin's three test suites, the environments they ru | Suite | Where | Runs on | Covers | | --- | --- | --- | --- | | Unit | `tests/Unit/` | PHP alone, without WordPress | the component tree and the footprint's shape and coverage | -| Integration | `tests/Integration/` | the tests site, with WordPress, WooCommerce and order tables loaded | the boot, every component, the options and their readers, the migrations, deactivation, uninstall, and the assets and blocks | +| Integration | `tests/Integration/` | the tests site, with WordPress, WooCommerce and order tables loaded | the boot, every component, the options and their readers, the migrations, deactivation, uninstall, the assets and blocks, and a partial deploy: a missing build file or engine, or an include that throws | | End to end | `tests/e2e/` | the tests site, in a browser through Playwright | the settings tab by keyboard, a rejected and a corrected value, accessibility through axe, and the plugin with WooCommerce deactivated | PHPUnit runs both PHP suites in random order, so each row sets up what it needs and restores what it changes; its cache lives in `tests/.cache/phpunit/`, and Playwright's output in `tests/.cache/artifacts/`. diff --git a/uninstall.php b/uninstall.php index dd28ad4..26b608e 100644 --- a/uninstall.php +++ b/uninstall.php @@ -32,6 +32,9 @@ $dws_tpl_footprint = dws_tpl_get_footprint(); $dws_tpl_consent = DWS_TPL_OPTION_DELETE_DATA; $dws_tpl_meta_types = MetaTypes::DEFINITIONS; + if ( ! class_exists( Uninstaller::class ) ) { + throw new RuntimeException( 'its uninstall engine is missing.' ); + } } catch ( Throwable $error ) { // WP-CLI then fails and keeps the files for a rerun on a complete build, while a web request deletes them and leaves the data. if ( defined( 'WP_CLI' ) && WP_CLI ) { From 6aedfe232e6ed161b5a3904bb4eb6ea799aad605 Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Thu, 29 Oct 2026 14:10:46 +0100 Subject: [PATCH 2/9] Show the greeting's translated default as a placeholder The Message field took the translated greeting as its default, so the first save of the tab stored it in the language of the admin who saved, and every customer then read that language. includes/migrations.php already keeps translated defaults out of the database for this reason; the field put one back. The field's default is empty and its placeholder shows the translated greeting, and the string reader reads an empty value of an option with a translated default as that default, so an untouched field keeps following the site's language. A cleared Message therefore greets with the default instead of printing nothing. Rows render the placeholder, save the untouched field and read the empty value. Assisted-by: Claude Code:claude-opus-5-5 --- docs/ARCHITECTURE.md | 2 +- includes/options.php | 8 ++++++-- src/Settings.php | 9 +++++---- tests/Integration/Feature/FeatureDetailTest.php | 4 ++-- tests/Integration/OptionsTest.php | 4 ++++ tests/Integration/SettingsTest.php | 12 +++++++++++- 6 files changed, 29 insertions(+), 10 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 118be5a..ff706c1 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -34,7 +34,7 @@ The request that activates the plugin does not boot it, because WordPress includ ## Data -- **Options** are prefixed constants in `includes/options.php`, read only through its typed readers, which guard each stored value and fall back to the key's default. Translated defaults come from `dws_tpl_get_option_text()` after `init`. +- **Options** are prefixed constants in `includes/options.php`, read only through its typed readers, which guard each stored value and fall back to the key's default. Translated defaults come from `dws_tpl_get_option_text()` after `init`; a field shows one as its placeholder and stores an empty value, which its reader reads as the default, so no save stores the admin's language. - **Migrations** are the append-only steps `dws_tpl_get_migration_steps()` returns. The framework's runner runs each pending step once per site on `wp_loaded`, behind a lock, and records the revision reached in `dws_tpl_revision`. A step converges from any partial state and throws when a write fails, so a failed step runs again on a later request. - **The footprint**, `dws_tpl_get_footprint()`, names every kind of data the plugin stores: option and user meta prefixes, network options, scheduled hooks and Action Scheduler groups. Options, transients and user meta under the `dws_tpl_` prefix are covered; meta on posts, terms, comments or orders needs a `meta_prefixes` entry per object type, and data under another name its own entry, which no check enforces. - **Deactivation** unschedules every hook in the footprint's `cron_hooks` and `as_hooks` on the current site. diff --git a/includes/options.php b/includes/options.php index 0042b25..45a790e 100644 --- a/includes/options.php +++ b/includes/options.php @@ -99,7 +99,7 @@ function dws_tpl_get_int_option( string $key ): int { } /** - * Returns an option as a string, which reads an option with a translated default only after init. + * Returns an option as a string, reading a missing or empty value of an option with a translated default as that default, only after init. * * @since 2.0.0 * @version 2.0.0 @@ -109,8 +109,12 @@ function dws_tpl_get_int_option( string $key ): int { * @return string */ function dws_tpl_get_string_option( string $key ): string { - $default = dws_tpl_get_option_default( $key ) ?? dws_tpl_get_option_text( $key ); + $text = dws_tpl_get_option_text( $key ); + $default = dws_tpl_get_option_default( $key ) ?? $text; $stored = get_option( $key, $default ); + if ( '' === $stored ) { + return $text; // The field shows a translated default as its placeholder, so an untouched field stores an empty value. + } return is_string( $stored ) ? $stored : ( is_string( $default ) ? $default : '' ); } diff --git a/src/Settings.php b/src/Settings.php index 34b72c9..533a523 100644 --- a/src/Settings.php +++ b/src/Settings.php @@ -51,10 +51,11 @@ public function get_section_fields( string $tab_id, string $section_id ): array 'default' => wc_bool_to_string( DWS_TPL_OPTION_DEFAULTS[ DWS_TPL_OPTION_FEATURE ] ), ), array( - 'type' => 'text', - 'id' => DWS_TPL_OPTION_GREETING, - 'title' => __( 'Message', 'dws-plugin-template' ), - 'default' => dws_tpl_get_option_text( DWS_TPL_OPTION_GREETING ), + 'type' => 'text', + 'id' => DWS_TPL_OPTION_GREETING, + 'title' => __( 'Message', 'dws-plugin-template' ), + 'default' => '', + 'placeholder' => dws_tpl_get_option_text( DWS_TPL_OPTION_GREETING ), // A default would be saved in the admin's language with the first save of the tab. ), array( 'type' => 'number', diff --git a/tests/Integration/Feature/FeatureDetailTest.php b/tests/Integration/Feature/FeatureDetailTest.php index 9d8c945..696158e 100644 --- a/tests/Integration/Feature/FeatureDetailTest.php +++ b/tests/Integration/Feature/FeatureDetailTest.php @@ -108,11 +108,11 @@ public function test_an_unsaved_greeting_prints_its_translated_default(): void { self::assertStringContainsString( esc_html( dws_tpl_get_option_text( DWS_TPL_OPTION_GREETING ) ), $this->print() ); } - public function test_an_empty_greeting_prints_nothing(): void { + public function test_an_empty_greeting_prints_its_translated_default(): void { update_option( DWS_TPL_OPTION_GREETING, '' ); $this->place_orders( 1 ); - self::assertSame( '', $this->print() ); + self::assertStringContainsString( esc_html( dws_tpl_get_option_text( DWS_TPL_OPTION_GREETING ) ), $this->print() ); } protected function place_orders( int $count ): void { diff --git a/tests/Integration/OptionsTest.php b/tests/Integration/OptionsTest.php index 1eec308..920d224 100644 --- a/tests/Integration/OptionsTest.php +++ b/tests/Integration/OptionsTest.php @@ -127,6 +127,10 @@ public function test_a_string_without_a_stored_value_reads_its_translated_defaul self::assertNotSame( '', dws_tpl_get_option_text( DWS_TPL_OPTION_GREETING ) ); self::assertSame( dws_tpl_get_option_text( DWS_TPL_OPTION_GREETING ), dws_tpl_get_string_option( DWS_TPL_OPTION_GREETING ) ); + update_option( DWS_TPL_OPTION_GREETING, '' ); + + self::assertSame( dws_tpl_get_option_text( DWS_TPL_OPTION_GREETING ), dws_tpl_get_string_option( DWS_TPL_OPTION_GREETING ) ); + update_option( DWS_TPL_OPTION_GREETING, 'Hello' ); self::assertSame( 'Hello', dws_tpl_get_string_option( DWS_TPL_OPTION_GREETING ) ); diff --git a/tests/Integration/SettingsTest.php b/tests/Integration/SettingsTest.php index ccb98b0..5a021e8 100644 --- a/tests/Integration/SettingsTest.php +++ b/tests/Integration/SettingsTest.php @@ -66,12 +66,22 @@ public function test_the_tab_shows_the_greeting_and_data_fields_with_their_defau self::assertSame( 'DWS Plugin Template', self::$tab->get_label() ); self::assertFalse( $form->getElementById( DWS_TPL_OPTION_FEATURE )?->hasAttribute( 'checked' ) ); - self::assertSame( dws_tpl_get_option_text( DWS_TPL_OPTION_GREETING ), $form->getElementById( DWS_TPL_OPTION_GREETING )?->getAttribute( 'value' ) ); + self::assertSame( '', $form->getElementById( DWS_TPL_OPTION_GREETING )?->getAttribute( 'value' ) ); + self::assertSame( dws_tpl_get_option_text( DWS_TPL_OPTION_GREETING ), $form->getElementById( DWS_TPL_OPTION_GREETING )?->getAttribute( 'placeholder' ) ); self::assertSame( '1', $form->getElementById( DWS_TPL_OPTION_GREETING_ORDERS )?->getAttribute( 'value' ) ); self::assertSame( '1', $form->getElementById( DWS_TPL_OPTION_GREETING_ORDERS )?->getAttribute( 'min' ) ); self::assertFalse( $form->getElementById( DWS_TPL_OPTION_DELETE_DATA )?->hasAttribute( 'checked' ) ); } + public function test_a_save_that_leaves_the_message_untouched_stores_no_translated_text(): void { + delete_option( DWS_TPL_OPTION_GREETING ); + + $this->save( array( DWS_TPL_OPTION_GREETING => $this->render()->getElementById( DWS_TPL_OPTION_GREETING )?->getAttribute( 'value' ) ) ); + + self::assertSame( '', get_option( DWS_TPL_OPTION_GREETING ) ); + self::assertSame( dws_tpl_get_option_text( DWS_TPL_OPTION_GREETING ), dws_tpl_get_string_option( DWS_TPL_OPTION_GREETING ) ); + } + public function test_the_retention_opt_in_round_trips_through_woocommerces_save(): void { $this->save( array( DWS_TPL_OPTION_DELETE_DATA => '1' ) ); From 81d8a91e7ea513dc9396f7c846251e9124af0b10 Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Thu, 29 Oct 2026 14:19:47 +0100 Subject: [PATCH 3/9] Refuse JSX characters in the scaffold name and build the rehearsal The fill-in refused only the characters that break the single-quoted PHP and JavaScript strings, but the display name also lands in the example block's JSX text, where <, >, { and } break the build. The Quality rehearsal never built or type-checked the filled-in scripts, so such a name passed it and the generated plugin failed its first build. The fill-in refuses <, >, { and } as well, and the rehearsal installs the filled-in plugin's npm packages, builds it and runs its type lint. Assisted-by: Claude Code:claude-opus-5-5 --- .github/scripts/fill-in-scaffold.sh | 4 ++-- .github/workflows/quality.yml | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/scripts/fill-in-scaffold.sh b/.github/scripts/fill-in-scaffold.sh index 2fa4d33..efddeeb 100755 --- a/.github/scripts/fill-in-scaffold.sh +++ b/.github/scripts/fill-in-scaffold.sh @@ -14,8 +14,8 @@ grep -Eq '^[A-Za-z][A-Za-z0-9]*$' <<< "$NS" || { echo "::error::php-namespace mu # Both ports land in JSON numbers, where a leading zero is invalid, and the tests environment takes the next one; # neither may be the template's own 8811 or 8812, which the leftover check below refuses. { grep -Eq '^[1-9][0-9]{0,4}$' <<< "$PORT" && [ "$PORT" -le 65534 ] && { [ "$PORT" -lt 8810 ] || [ "$PORT" -gt 8812 ]; }; } || { echo "::error::wp-env-port must be 1-65534 with no leading zero, and neither it nor the next port may be 8811 or 8812."; exit 1; } -# The display name lands in single-quoted PHP and JavaScript strings. -if printf '%s' "$PLUGIN_NAME" | grep -q "['\\\$/]"; then echo "::error::plugin-name must not contain \\, \$, / or '."; exit 1; fi +# The display name lands in single-quoted PHP and JavaScript strings, and in JSX text. +if printf '%s' "$PLUGIN_NAME" | grep -q "['\\\$/<>{}]"; then echo "::error::plugin-name must not contain \\, \$, /, ', <, >, { or }."; exit 1; fi export SCREAMING_PREFIX="${PREFIX^^}" export TESTS_PORT=$(( PORT + 1 )) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index d0e126d..fbeaed6 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -100,3 +100,13 @@ jobs: run: | composer lint:php composer test:unit + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: package.json + cache: 'npm' + # The display name lands in the block's JSX, which only a build and the type lint parse. + - name: Build and type-check the filled-in plugin's scripts + run: | + npm ci + npm run build + npm run lint:types From a14c5db8d96ada2ff56f1449a837577c1f46f0f3 Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Thu, 29 Oct 2026 14:19:47 +0100 Subject: [PATCH 4/9] Declare TypeScript and the Node types as devDependencies lint:types runs tsc over the blocks, assets and end-to-end specs, and the specs read Node's types, but neither typescript nor @types/node was declared: both arrived through @wordpress/scripts, so a change in its tree could swap the compiler's major or drop the types without a change here. Both are declared at the versions the lock already resolves, so the lock moves only its root entries. Assisted-by: Claude Code:claude-opus-5-5 --- package-lock.json | 5 +++-- package.json | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 84d0c10..285fb1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,13 +10,15 @@ "@ahegyes/wordpress-configs": "git+https://github.com/ahegyes/wordpress-configs.git#9f64dc77b25727130884eaf427d2bd75493557ce", "@axe-core/playwright": "^4.14.0", "@playwright/test": "^1.64.1", + "@types/node": "^26.6.5", "@types/react": "^19.3.0", "@wordpress/block-editor": "^19.0.0", "@wordpress/blocks": "^17.0.0", "@wordpress/e2e-test-utils-playwright": "^3.1.0", "@wordpress/env": "^12.0.0", "@wordpress/i18n": "^7.0.0", - "@wordpress/scripts": "^37.0.0" + "@wordpress/scripts": "^37.0.0", + "typescript": "^6.0.3" }, "engines": { "node": ">=26", @@ -23811,7 +23813,6 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 4095178..a503ab7 100644 --- a/package.json +++ b/package.json @@ -16,13 +16,15 @@ "@ahegyes/wordpress-configs": "git+https://github.com/ahegyes/wordpress-configs.git#9f64dc77b25727130884eaf427d2bd75493557ce", "@axe-core/playwright": "^4.14.0", "@playwright/test": "^1.64.1", + "@types/node": "^26.6.5", "@types/react": "^19.3.0", "@wordpress/block-editor": "^19.0.0", "@wordpress/blocks": "^17.0.0", "@wordpress/e2e-test-utils-playwright": "^3.1.0", "@wordpress/env": "^12.0.0", "@wordpress/i18n": "^7.0.0", - "@wordpress/scripts": "^37.0.0" + "@wordpress/scripts": "^37.0.0", + "typescript": "^6.0.3" }, "scripts": { "setup": "npm run packages-install:all && npm run build && npm run wp-env:start", @@ -46,11 +48,9 @@ "start": "npm run start:blocks & npm run start:assets & wait", "start:blocks": "wp-scripts start --webpack-src-dir=blocks/src --output-path=blocks/build --blocks-manifest", "start:assets": "wp-scripts start editor=./assets/src/editor.ts greeting=./assets/src/greeting.scss --output-path=assets/build", - "lint:scripts": "wp-scripts lint-js blocks/src assets/src tests/e2e", "lint:styles": "wp-scripts lint-style 'blocks/src/**/*.scss' 'assets/src/**/*.scss'", "lint:types": "tsc", - "audit": "composer audit && npm audit", "audit:fix": "npm audit fix", "check:engines": "wp-scripts check-engines", From 1c92b25a651728898b4637f78d8e2571b177993e Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Thu, 29 Oct 2026 14:23:18 +0100 Subject: [PATCH 5/9] Give the recipes' number fields defaults and a typed read The two settings recipes declared their limit field without a default, so a fresh settings page rendered it empty instead of at the default the recipe's constant holds, and the recurring-jobs recipe read its repair timestamp with a raw filter_var() on get_option(), which AGENTS.md reserves to the typed readers. Both number fields take the default from DWS_TPL_SETTING_DEFAULTS, and the repair gate reads through dws_tpl_get_int_option(). Assisted-by: Claude Code:claude-opus-5-5 --- docs/recipes.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/recipes.md b/docs/recipes.md index 50ae9bc..75a4013 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -326,6 +326,7 @@ final class SettingsPage implements ComponentInterface, OptionsPageDefinitionInt 'type' => 'number', 'id' => DWS_TPL_SETTING_LIMIT, 'title' => __( 'Limit', 'dws-plugin-template' ), + 'default' => (string) DWS_TPL_SETTING_DEFAULTS[ DWS_TPL_SETTING_LIMIT ], 'custom_attributes' => array( 'min' => '1', 'step' => '1', @@ -708,7 +709,7 @@ final class Pruning implements ComponentInterface { * @return void */ public function repair_schedule(): void { - $ensured_at = filter_var( get_option( DWS_TPL_OPTION_RECURRING_ENSURED_AT, 0 ), FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE ) ?? 0; + $ensured_at = dws_tpl_get_int_option( DWS_TPL_OPTION_RECURRING_ENSURED_AT ); if ( time() - $ensured_at < HOUR_IN_SECONDS ) { return; } @@ -1041,6 +1042,7 @@ final class Settings implements ComponentInterface, OptionsPageDefinitionInterfa 'type' => 'number', 'id' => DWS_TPL_SETTING_LIMIT, 'title' => __( 'Limit', 'dws-plugin-template' ), + 'default' => (string) DWS_TPL_SETTING_DEFAULTS[ DWS_TPL_SETTING_LIMIT ], 'custom_attributes' => array( 'min' => '1', 'step' => '1', From f68cfd57d866dd3f92df8fe39f7ac835f5824c5a Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Thu, 29 Oct 2026 14:23:19 +0100 Subject: [PATCH 6/9] Bring the template's prose in line with what it ships Several documents described a template that no longer exists or overstated what it checks. CONTRIBUTING's release steps used arrows and never set the entry file's Version header, which the release refuses to tag without. The issue templates offered a Plugin singleton and a WelcomeNotice the template no longer has. The README listed safeguards where the template ships an empty slot. The entry's Description differed from composer.json's sentence, and the scaffold's TODO left package.json's description out. SECURITY.md said the audit fails on any npm advisory, though it audits only runtime dependencies, of which there are none. Convention 5 implied an absent boolean reads its default, though FILTER_VALIDATE_BOOL reads '' as false. The tests README said Unit runs before the WordPress matrix, which does not wait for it. Each now states what the template does. Assisted-by: Claude Code:claude-opus-5-5 --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- AGENTS.md | 2 +- CONTRIBUTING.md | 8 +++++--- README.md | 2 +- README.scaffold.md | 2 +- SECURITY.md | 2 +- dws-plugin-template.php | 2 +- tests/README.md | 2 +- 9 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index b88e3ee..f77ccff 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -7,7 +7,7 @@ body: attributes: label: Affected area options: - - Plugin runtime (Plugin singleton, WelcomeNotice, framework wiring) + - Plugin runtime (composition root, components, framework wiring) - Composer scoping pipeline (scoper.inc.php, post-autoload-dump) - Changelog tooling (composer changelog:* scripts) - CI workflows (quality.yml, tests.yml) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 9a5659a..7f6f04b 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -7,7 +7,7 @@ body: attributes: label: Target area options: - - Plugin runtime scaffold (Plugin, WelcomeNotice, framework wiring) + - Plugin runtime scaffold (composition root, components, framework wiring) - Composer scoping pipeline - Changelog tooling - CI workflows diff --git a/AGENTS.md b/AGENTS.md index 559a291..3062dbd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,7 +52,7 @@ Both run PHP 8.5 with WooCommerce from `vendor/` and its order tables on; CI run 2. **Variants.** A component that exists only in one build variant is a child in a branch of its parent's `get_child_component_classes()`, never a second entry in `COMPONENTS`. 3. **Request context.** `is_admin()` is false on REST requests and true in admin-ajax ones, so an admin-only feature decides the request context in its callbacks, never in `should_load()`. 4. **Add-ons.** An add-on extends a base plugin only through the base plugin's documented hooks. -5. **Request reads.** An integer or boolean is read as `filter_var( wp_unslash( $_POST['key'] ?? '' ), FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE ) ?? $default` (`FILTER_VALIDATE_BOOL` for a boolean), and a string through an `is_string()` guard and the field's sanitizer; never `filter_input()`, the `default` filter option or a cast on request input. +5. **Request reads.** An integer or boolean is read as `filter_var( wp_unslash( $_POST['key'] ?? '' ), FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE ) ?? $default` (`FILTER_VALIDATE_BOOL` for a boolean, where an absent key reads false, as an unchecked checkbox posts nothing), and a string through an `is_string()` guard and the field's sanitizer; never `filter_input()`, the `default` filter option or a cast on request input. 6. **Options and data.** Options are read only through `includes/options.php`'s typed readers, safeguards excepted. Everything stored lives under `dws_tpl_`, or `_dws_tpl_` for hidden meta. The footprint covers the prefix's options, transients and user meta; object meta needs a `meta_prefixes` entry per type, and anything else its own entry. 7. **Safeguards** live in `includes/safeguards.php`, call only WordPress and that file, and relax only through their own filter, through `dws_tpl_is_relaxed()` (`docs/recipes.md`). 8. **Migrations** are append-only: a step converges from any partial state, verifies its writes and throws when one fails. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7a1abfb..b2c0bc0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,18 +2,20 @@ ## Changelog entries -One fragment per user-visible change so PRs don't conflict on `CHANGELOG.md`: +Each user-visible change adds one fragment, so pull requests never conflict on `CHANGELOG.md`: ```bash composer changelog:add ``` -Interactive prompt (Significance + Type + entry); fragment lands in `changelog/`, commit it with the PR. CI validates via `composer changelog:validate`. +The command asks for the change's significance, its type and its entry, and writes the fragment to `changelog/`, which the pull request commits. CI validates the fragments with `composer changelog:validate`. ## Releases +A release writes the fragments into a new version block of `CHANGELOG.md`, whose version the fragments' significance sets: + ```bash composer changelog:write ``` -Aggregates fragments → new `CHANGELOG.md` version block (semver computed from fragment significance). Commit + tag. +The release commit also sets the entry file's `Version:` header to that version, and `readme.txt`'s `Stable tag:` where the plugin has one, because the release refuses a tag either differs from. Pushing the tag `v` on that commit starts the release. diff --git a/README.md b/README.md index 9b55319..b5796d7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ WordPress plugins on the wp-framework packages start here, from one tested anatomy, so all are built alike and a fix found in one applies to all. -The template is itself a working WooCommerce plugin: it boots the framework's composition, holds a settings tab, a feature gated on one of its settings, a block, migrations, safeguards and an uninstall footprint, and every suite and release check passes on it. `docs/ARCHITECTURE.md` describes that anatomy and `docs/recipes.md` the changes most plugins make to it. +The template is itself a working WooCommerce plugin: it boots the framework's composition, holds a settings tab, a feature gated on one of its settings, a block, migrations, an empty safeguards slot and an uninstall footprint, and every suite and release check passes on it. `docs/ARCHITECTURE.md` describes that anatomy and `docs/recipes.md` the changes most plugins make to it. ## Starting a plugin diff --git a/README.scaffold.md b/README.scaffold.md index d21ded2..77d16aa 100644 --- a/README.scaffold.md +++ b/README.scaffold.md @@ -1,6 +1,6 @@ # DWS Plugin Template -TODO: one sentence saying why this plugin exists, its philosophy rather than a list of what it holds. The same sentence is `composer.json`'s `description`, the entry file's `Description` header and the repository's GitHub description. +TODO: one sentence saying why this plugin exists, its philosophy rather than a list of what it holds. The same sentence is `composer.json`'s and `package.json`'s `description`, the entry file's `Description` header and the repository's GitHub description. ## Requirements diff --git a/SECURITY.md b/SECURITY.md index 7708d50..b28bea2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -11,4 +11,4 @@ In scope: the plugin's PHP, JavaScript and CSS, its prefixing configuration (`sc Out of scope, reported upstream instead: - plugins generated from this repository, to their maintainers; - the wp-framework packages, to `ahegyes/wordpress-framework`; -- WordPress, WooCommerce and the other Composer and npm dependencies, to their projects. Composer refuses to resolve a dependency version with a known advisory through `roave/security-advisories`, and the Supply-Chain Audit workflow fails on one. +- WordPress, WooCommerce and the other Composer and npm dependencies, to their projects. Composer refuses to resolve a dependency version with a known advisory through `roave/security-advisories`. The Supply-Chain Audit workflow fails on an advisory anywhere in the Composer lock, and on a high-severity one among the npm runtime dependencies, of which the template has none; advisories in the npm development tools reach Dependabot alerts instead. diff --git a/dws-plugin-template.php b/dws-plugin-template.php index 1038e8a..8e0a8cd 100644 --- a/dws-plugin-template.php +++ b/dws-plugin-template.php @@ -2,7 +2,7 @@ /** * Plugin Name: DWS Plugin Template * Plugin URI: https://github.com/ahegyes/wordpress-plugin-template - * Description: Starting point for WordPress plugins built on the DWS framework. + * Description: WordPress plugins on the wp-framework packages start here, from one tested anatomy, so all are built alike and a fix found in one applies to all. * Version: 2.0.0 * Requires at least: 7.1 * Requires PHP: 8.5 diff --git a/tests/README.md b/tests/README.md index 0fcfda5..3dddf0f 100644 --- a/tests/README.md +++ b/tests/README.md @@ -48,7 +48,7 @@ composer quality-check ## Continuous integration -- **Tests** runs Unit, then Integration and the end-to-end suite once per WordPress branch, from the entry file's `Requires at least` up to the newest release, which WordPress's version API lists, so a new release joins without an edit. +- **Tests** runs Unit once, and Integration and the end-to-end suite once per WordPress branch, from the entry file's `Requires at least` up to the newest release, which WordPress's version API lists, so a new release joins without an edit. - **Tests (nightly)** runs Integration and the end-to-end suite on WordPress trunk every day, outside pull requests. - **Release** builds the zip on every pull request as a tag would, runs Plugin Check and the archive checks, then activates the zip on a site and runs the end-to-end suite against it. - **Quality** runs PHPCS, PHPStan, the PHP 7.4 parse of the entry file, `uninstall.php` and `includes/`, the script, style and type lints, the `block.json` schema and the changelog fragments. From 7adb5953add87616efd91e375399c07ce6e9dc8d Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Thu, 29 Oct 2026 16:00:39 +0100 Subject: [PATCH 7/9] Format the swept scripts so short plugin names pass lint:scripts The fill-in swaps the plugin's name and slug into Prettier-formatted TSX. A name or slug shorter than the template's own leaves lines that Prettier would now join, so a plugin generated as "Acme Orders" failed lint:scripts on its first Quality run, while a long name passed, and the rehearsal ran no script lint to notice. After the sweep, the fill-in installs the plugin's npm packages with install scripts off, since its job holds a token that can push, and formats the JS and TS files lint:scripts covers with wp-scripts format. Both the Fill in scaffold workflow and the rehearsal set up Node before the fill-in, and the rehearsal runs lint:scripts with the build and type lint. A short and a long name each pass all three, and formatting changes only the two lines the shorter name shortened. Assisted-by: Claude Code:claude-opus-5-5 --- .github/scripts/fill-in-scaffold.sh | 5 +++++ .github/workflows/fill-in-scaffold.yml | 4 ++++ .github/workflows/quality.yml | 14 +++++++------- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/scripts/fill-in-scaffold.sh b/.github/scripts/fill-in-scaffold.sh index efddeeb..fa6577f 100755 --- a/.github/scripts/fill-in-scaffold.sh +++ b/.github/scripts/fill-in-scaffold.sh @@ -65,6 +65,11 @@ for swept_json in .wp-env.json .wp-env.tests.json composer.json package.json pac php -r 'json_decode( (string) file_get_contents( $argv[1] ), false, 512, JSON_THROW_ON_ERROR );' "$swept_json" done +# A name or slug shorter than the template's leaves wrapped lines that Prettier joins, so the swept scripts are formatted +# again. Install scripts stay off, because the fill-in job holds a token that can push. +npm ci --ignore-scripts --no-audit --no-fund +node_modules/.bin/wp-scripts format '{blocks/src,assets/src,tests/e2e}/**/*.{js,jsx,ts,tsx}' + status=0 if git grep -n -I -E 'dws-plugin-template|dws_tpl|DWS_TPL|PluginTemplate|DWS Plugin Template|wordpress-plugin-template' -- . ':!.github/scripts/fill-in-scaffold.sh'; then echo "::error::The fill-in left the template's names above." diff --git a/.github/workflows/fill-in-scaffold.yml b/.github/workflows/fill-in-scaffold.yml index af57e74..6d02d25 100644 --- a/.github/workflows/fill-in-scaffold.yml +++ b/.github/workflows/fill-in-scaffold.yml @@ -48,6 +48,10 @@ jobs: coverage: none tools: composer:v2 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: package.json + - name: Fill in the placeholders env: PLUGIN_NAME: ${{ inputs.plugin-name }} diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index fbeaed6..f871d69 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -85,6 +85,10 @@ jobs: php-version: '8.5' coverage: none tools: composer:v2 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: package.json + cache: 'npm' - name: Fill in the scaffold env: PLUGIN_NAME: Acme Orders @@ -100,13 +104,9 @@ jobs: run: | composer lint:php composer test:unit - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version-file: package.json - cache: 'npm' - # The display name lands in the block's JSX, which only a build and the type lint parse. - - name: Build and type-check the filled-in plugin's scripts + # The display name lands in the block's JSX, which only a build and the script and type lints parse. + - name: Build and lint the filled-in plugin's scripts run: | - npm ci npm run build + npm run lint:scripts npm run lint:types From 0aeb2dca3041994e842a8434608e98d9028c420e Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Thu, 29 Oct 2026 16:04:15 +0100 Subject: [PATCH 8/9] Move the wordpress-configs pins to its current trunk The workflows, package.json and the locks pinned configs at 9f64dc7, before configs merged its re-audit follow-ups and the release's tool lock. The template's own release still took WP-CLI from setup-php and checked neither the plugin's own PHP floor nor phpunit.dist.xml. Every workflow ref, the npm dependency and both locks move to 3cbd985. The release now installs its WP-CLI commands from configs' php/release lock, which the template's development install provides, since it requires configs under require-dev; no reusable workflow's inputs change, and each lock changes only the configs entry. Assisted-by: Claude Code:claude-opus-5-5 --- .github/workflows/audit.yml | 2 +- .github/workflows/codeql.yml | 2 +- .github/workflows/quality.yml | 8 ++++---- .github/workflows/release.yml | 2 +- .github/workflows/tests-nightly.yml | 4 ++-- .github/workflows/tests.yml | 6 +++--- .github/workflows/workflow-checks.yml | 2 +- composer.lock | 8 ++++---- package-lock.json | 9 +++++---- package.json | 2 +- 10 files changed, 23 insertions(+), 22 deletions(-) diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 212883a..3559bf3 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -16,6 +16,6 @@ jobs: audit: name: Audit # npm gate audits production deps only — the dev-tooling graph (wp-scripts/playwright trees) is advisory via dependabot alerts. - uses: ahegyes/wordpress-configs/.github/workflows/reusable-supply-chain-audit.yml@9f64dc77b25727130884eaf427d2bd75493557ce + uses: ahegyes/wordpress-configs/.github/workflows/reusable-supply-chain-audit.yml@3cbd985255e76f8026644ccdee5be0b2b2da312b with: npm-audit-flags: '--omit=dev --audit-level=high' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 18ef77a..cc091ec 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -20,4 +20,4 @@ jobs: actions: read contents: read security-events: write - uses: ahegyes/wordpress-configs/.github/workflows/reusable-codeql.yml@9f64dc77b25727130884eaf427d2bd75493557ce + uses: ahegyes/wordpress-configs/.github/workflows/reusable-codeql.yml@3cbd985255e76f8026644ccdee5be0b2b2da312b diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index f871d69..4b91e5e 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -11,20 +11,20 @@ permissions: jobs: lint-php: name: PHP Lint - uses: ahegyes/wordpress-configs/.github/workflows/reusable-php-lint.yml@9f64dc77b25727130884eaf427d2bd75493557ce + uses: ahegyes/wordpress-configs/.github/workflows/reusable-php-lint.yml@3cbd985255e76f8026644ccdee5be0b2b2da312b with: php-version: '8.5' scripts: '["lint:php:phpcs", "lint:php:phpcs:tests", "lint:php:phpstan"]' lint-scripts-styles: name: Scripts and styles lint - uses: ahegyes/wordpress-configs/.github/workflows/reusable-scripts-styles-lint.yml@9f64dc77b25727130884eaf427d2bd75493557ce + uses: ahegyes/wordpress-configs/.github/workflows/reusable-scripts-styles-lint.yml@3cbd985255e76f8026644ccdee5be0b2b2da312b with: scripts: '["lint:scripts", "lint:styles", "lint:types"]' block-json: name: block.json schema - uses: ahegyes/wordpress-configs/.github/workflows/reusable-block-json-check.yml@9f64dc77b25727130884eaf427d2bd75493557ce + uses: ahegyes/wordpress-configs/.github/workflows/reusable-block-json-check.yml@3cbd985255e76f8026644ccdee5be0b2b2da312b slug: name: Plugin slug @@ -50,7 +50,7 @@ jobs: php-syntax: name: PHP syntax needs: slug - uses: ahegyes/wordpress-configs/.github/workflows/reusable-php-syntax-check.yml@9f64dc77b25727130884eaf427d2bd75493557ce + uses: ahegyes/wordpress-configs/.github/workflows/reusable-php-syntax-check.yml@3cbd985255e76f8026644ccdee5be0b2b2da312b with: php-versions: '["7.4"]' paths: ${{ format('["{0}.php", "uninstall.php", "includes"]', needs.slug.outputs.slug) }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 26162f5..9a8ab70 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,7 +42,7 @@ jobs: permissions: actions: read contents: write - uses: ahegyes/wordpress-configs/.github/workflows/reusable-release.yml@9f64dc77b25727130884eaf427d2bd75493557ce + uses: ahegyes/wordpress-configs/.github/workflows/reusable-release.yml@3cbd985255e76f8026644ccdee5be0b2b2da312b with: project-path: '.' plugin-slug: ${{ needs.slug.outputs.slug }} diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index 211133d..b5613b4 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -12,7 +12,7 @@ permissions: jobs: integration: name: Integration - uses: ahegyes/wordpress-configs/.github/workflows/reusable-phpunit.yml@9f64dc77b25727130884eaf427d2bd75493557ce + uses: ahegyes/wordpress-configs/.github/workflows/reusable-phpunit.yml@3cbd985255e76f8026644ccdee5be0b2b2da312b with: php-version: '8.5' wp-env-core: 'https://wordpress.org/nightly-builds/wordpress-latest.zip' @@ -21,7 +21,7 @@ jobs: e2e: name: E2E - uses: ahegyes/wordpress-configs/.github/workflows/reusable-playwright-e2e.yml@9f64dc77b25727130884eaf427d2bd75493557ce + uses: ahegyes/wordpress-configs/.github/workflows/reusable-playwright-e2e.yml@3cbd985255e76f8026644ccdee5be0b2b2da312b with: artifact-slug: nightly php-version: '8.5' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 752b63f..d780ecf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ permissions: jobs: unit: name: Unit - uses: ahegyes/wordpress-configs/.github/workflows/reusable-phpunit.yml@9f64dc77b25727130884eaf427d2bd75493557ce + uses: ahegyes/wordpress-configs/.github/workflows/reusable-phpunit.yml@3cbd985255e76f8026644ccdee5be0b2b2da312b with: php-version: '8.5' composer-script: 'test:unit' @@ -52,7 +52,7 @@ jobs: fail-fast: false matrix: wp: ${{ fromJSON(needs.wordpress.outputs.versions) }} - uses: ahegyes/wordpress-configs/.github/workflows/reusable-phpunit.yml@9f64dc77b25727130884eaf427d2bd75493557ce + uses: ahegyes/wordpress-configs/.github/workflows/reusable-phpunit.yml@3cbd985255e76f8026644ccdee5be0b2b2da312b with: php-version: '8.5' wp-version: ${{ matrix.wp }} @@ -66,7 +66,7 @@ jobs: fail-fast: false matrix: wp: ${{ fromJSON(needs.wordpress.outputs.versions) }} - uses: ahegyes/wordpress-configs/.github/workflows/reusable-playwright-e2e.yml@9f64dc77b25727130884eaf427d2bd75493557ce + uses: ahegyes/wordpress-configs/.github/workflows/reusable-playwright-e2e.yml@3cbd985255e76f8026644ccdee5be0b2b2da312b with: artifact-slug: wp-${{ matrix.wp }} php-version: '8.5' diff --git a/.github/workflows/workflow-checks.yml b/.github/workflows/workflow-checks.yml index dc6460d..880dc25 100644 --- a/.github/workflows/workflow-checks.yml +++ b/.github/workflows/workflow-checks.yml @@ -24,4 +24,4 @@ jobs: contents: read security-events: write actions: read - uses: ahegyes/wordpress-configs/.github/workflows/reusable-workflow-checks.yml@9f64dc77b25727130884eaf427d2bd75493557ce + uses: ahegyes/wordpress-configs/.github/workflows/reusable-workflow-checks.yml@3cbd985255e76f8026644ccdee5be0b2b2da312b diff --git a/composer.lock b/composer.lock index f148f46..2d62b34 100644 --- a/composer.lock +++ b/composer.lock @@ -13,12 +13,12 @@ "source": { "type": "git", "url": "https://github.com/ahegyes/wordpress-configs.git", - "reference": "9f64dc77b25727130884eaf427d2bd75493557ce" + "reference": "3cbd985255e76f8026644ccdee5be0b2b2da312b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ahegyes/wordpress-configs/zipball/9f64dc77b25727130884eaf427d2bd75493557ce", - "reference": "9f64dc77b25727130884eaf427d2bd75493557ce", + "url": "https://api.github.com/repos/ahegyes/wordpress-configs/zipball/3cbd985255e76f8026644ccdee5be0b2b2da312b", + "reference": "3cbd985255e76f8026644ccdee5be0b2b2da312b", "shasum": "" }, "require": { @@ -139,7 +139,7 @@ "source": "https://github.com/ahegyes/wordpress-configs/tree/trunk", "issues": "https://github.com/ahegyes/wordpress-configs/issues" }, - "time": "2026-10-28T20:26:01+00:00" + "time": "2026-10-29T14:52:37+00:00" }, { "name": "ahegyes/wp-framework-bootstrap", diff --git a/package-lock.json b/package-lock.json index 285fb1c..f9ec75a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,7 @@ "name": "@ahegyes/dws-plugin-template", "license": "GPL-2.0-or-later", "devDependencies": { - "@ahegyes/wordpress-configs": "git+https://github.com/ahegyes/wordpress-configs.git#9f64dc77b25727130884eaf427d2bd75493557ce", + "@ahegyes/wordpress-configs": "git+https://github.com/ahegyes/wordpress-configs.git#3cbd985255e76f8026644ccdee5be0b2b2da312b", "@axe-core/playwright": "^4.14.0", "@playwright/test": "^1.64.1", "@types/node": "^26.6.5", @@ -27,8 +27,8 @@ }, "node_modules/@ahegyes/wordpress-configs": { "version": "0.0.0", - "resolved": "git+ssh://git@github.com/ahegyes/wordpress-configs.git#9f64dc77b25727130884eaf427d2bd75493557ce", - "integrity": "sha512-Yk/hpkF7NQoZHAgDgByPOV5zENW9oaeb58MdX9fcOKlnlJs0C+lwI7DLBrVVmzNzBNrLkBCO6hVPoYH8/xeFhw==", + "resolved": "git+ssh://git@github.com/ahegyes/wordpress-configs.git#3cbd985255e76f8026644ccdee5be0b2b2da312b", + "integrity": "sha512-zTqkUCRwgWiUbcr6mm6QcOgObxrz+m1lYuybslhMRm5DHg2ZQC0zwi/cxNZAB/+AZduTfTbzHM+nu1Dpw523/Q==", "dev": true, "license": "MIT", "dependencies": { @@ -43,7 +43,8 @@ "@wordpress/eslint-plugin": ">=28.0.0", "@wordpress/postcss-plugins-preset": ">=6.0.0", "@wordpress/scripts": ">=37.0.0", - "@wordpress/stylelint-config": ">=27.0.0" + "@wordpress/stylelint-config": ">=27.0.0", + "typescript": ">=6.0.0" } }, "node_modules/@ariakit/components": { diff --git a/package.json b/package.json index a503ab7..611025c 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "npm": ">=11" }, "devDependencies": { - "@ahegyes/wordpress-configs": "git+https://github.com/ahegyes/wordpress-configs.git#9f64dc77b25727130884eaf427d2bd75493557ce", + "@ahegyes/wordpress-configs": "git+https://github.com/ahegyes/wordpress-configs.git#3cbd985255e76f8026644ccdee5be0b2b2da312b", "@axe-core/playwright": "^4.14.0", "@playwright/test": "^1.64.1", "@types/node": "^26.6.5", From f644b9ed30ea517f746c65e04a2c623b23010404 Mon Sep 17 00:00:00 2001 From: Tony Hegyes Date: Thu, 29 Oct 2026 16:12:00 +0100 Subject: [PATCH 9/9] Keep the scaffold README out of the template's release zip The template's .distignore excluded README.md but not README.scaffold.md, the README a generated plugin starts from, so the template's own release archive shipped a file that describes filling in a plugin. .distignore excludes README.scaffold.md, and the fill-in drops that line when it moves the file to README.md, which .distignore already excludes, so a generated plugin keeps no exclusion for a file it lacks. Assisted-by: Claude Code:claude-opus-5-5 --- .distignore | 1 + .github/scripts/fill-in-scaffold.sh | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.distignore b/.distignore index ee1bef4..20344fb 100644 --- a/.distignore +++ b/.distignore @@ -15,6 +15,7 @@ CHANGELOG.md CLAUDE.md CONTRIBUTING.md README.md +README.scaffold.md SECURITY.md changelog/ diff --git a/.github/scripts/fill-in-scaffold.sh b/.github/scripts/fill-in-scaffold.sh index fa6577f..2301e76 100755 --- a/.github/scripts/fill-in-scaffold.sh +++ b/.github/scripts/fill-in-scaffold.sh @@ -22,6 +22,8 @@ export TESTS_PORT=$(( PORT + 1 )) export REPO_NAME="${REPO#*/}" git mv --force README.scaffold.md README.md +# The scaffold README becomes the plugin's README.md, which .distignore already excludes. +perl -ni -e 'print unless /^README\.scaffold\.md$/' .distignore git rm --quiet includes/_disabled-example.php # The template's recipe test loads the recipe's safeguard helper, which a plugin that applies the recipe declares # itself, so only the recipe's own test stays.