From 4caf1fbbdca6884199ce016b138fd42596d328cd Mon Sep 17 00:00:00 2001 From: Akshat Date: Sun, 30 Aug 2026 12:43:14 +0530 Subject: [PATCH] fix(templates): bump Tested up to 7.1, robust version compare, REST item schema (A, B, C) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A — TESTED_UP_TO - Extract a TESTED_UP_TO constant ('7.1', was inline '6.9' two majors stale) next to MIN_PHP, and a versionGte() helper comparing dot-separated integer segments instead of parseFloat() (which mis-parses two-digit segments: parseFloat('6.10') === 6.1). B — dangling {@see Services} in accessor-less builds - Plugin.php's class docblock only mentions the Services locator under {{#if has_services}} now, matching the same accessor-conditional wiring #16 already applied to Services.php/Plugin_TestCase.php. Reflowed the two sentences so the accessor-less docblock still reads cleanly rather than leaving an orphaned clause. C — REST controller shipped no schema - Rest_Controller::get_item_schema() now describes get_items()'s actual response shape (message: string, param: string|null), wired into register_routes() via 'schema' => $this->get_public_item_schema( ... ) (first-class callable, matching the file's existing style). - Extended the WP_REST_Controller unit-test stub (tests/bootstrap.php) with $schema, get_public_item_schema(), and add_additional_fields_schema() — real WP core provides these; the minimal stub didn't, which would have fataled every generated test that hits register_routes() now that it calls get_public_item_schema(). - New Rest_Controller_Test covering the schema shape. Verified beyond the CLI's own 69/69 suite: generated a real --modules rest_api,caching scaffold, ran composer install + composer test for real (12/12 PHPUnit tests, 17 assertions — the __() stub gap in the new test was only caught this way) and composer lint (15/15 files, 0 errors/0 warnings). Also confirmed the has_services docblock split renders cleanly in both an accessor-less and an accessor-bearing scaffold, and Tested up to renders 7.1. Addresses open-issues report items A, B, C. --- index.js | 29 ++++++++++++-- templates/src/Plugin.php | 4 +- templates/src/Rest/Rest_Controller.php | 37 ++++++++++++++++++ templates/tests/Unit/Rest_Controller_Test.php | 15 ++++++++ templates/tests/bootstrap.php | 38 +++++++++++++++++++ 5 files changed, 117 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index 4570754..6815b07 100644 --- a/index.js +++ b/index.js @@ -321,6 +321,27 @@ export function validateModules(modules) { // still getting security fixes, and matches the widest install base. export const MIN_PHP = '8.2'; +// readme.txt "Tested up to" seed — a generated scaffold can't know the WP +// release it'll actually be tested against, so this is a recent stable floor +// the developer bumps per release, never a claim of real testing. Bump this +// one line as WordPress releases; wp.org shows an "untested with your version" +// warning once a plugin's declared value trails current by much. +export const TESTED_UP_TO = '7.1'; + +// Dot-separated numeric version compare (WordPress/PHP-style "x.y" or +// "x.y.z" strings only — no pre-release suffixes). `parseFloat` on a version +// string mis-parses two-digit segments (parseFloat('6.10') === 6.1), so this +// compares each segment as its own integer instead. +export function versionGte(a, b) { + const as = a.split('.').map(Number); + const bs = b.split('.').map(Number); + for (let i = 0; i < Math.max(as.length, bs.length); i++) { + const diff = (as[i] || 0) - (bs[i] || 0); + if (diff !== 0) return diff > 0; + } + return true; +} + export function validateOutputDir(val) { if (!val || typeof val !== 'string' || val.trim().length === 0) { return 'Output directory is required.'; @@ -884,10 +905,10 @@ function scaffoldInto(answers, targetDir) { '{{DESCRIPTION}}': answers.description, '{{MIN_PHP}}': MIN_PHP, '{{REQUIRES_AT_LEAST}}': requiredWpVersion, - // readme.txt "Tested up to". A generated scaffold can't know the WP - // release it'll be tested against, so seed a recent stable floor the - // developer bumps per release — never below what the plugin requires. - '{{TESTED_UP_TO}}': parseFloat(requiredWpVersion) > 6.9 ? requiredWpVersion : '6.9', + // readme.txt "Tested up to" — see TESTED_UP_TO. Never below what the + // plugin requires (a block-editor module can push requiredWpVersion + // past the seed). + '{{TESTED_UP_TO}}': versionGte(requiredWpVersion, TESTED_UP_TO) ? requiredWpVersion : TESTED_UP_TO, '{{VERSION}}': '1.0.0', '{{YEAR}}': new Date().getFullYear().toString(), '{{PLUGIN_HEADER_EXTRA}}': pluginHeaderExtra, diff --git a/templates/src/Plugin.php b/templates/src/Plugin.php index 22909d6..f3cd7a5 100644 --- a/templates/src/Plugin.php +++ b/templates/src/Plugin.php @@ -19,8 +19,8 @@ * Singleton bootloader for {{PLUGIN_NAME}}. * * Not a service container: modules are plain classes with an init_hooks() - * method, and shared services come from {@see Services}. One instance per - * request, reached with instance(); boot() is idempotent. + * method.{{#if has_services}} Shared services come from {@see Services}.{{/if}} + * One instance per request, reached with instance(); boot() is idempotent. */ final class Plugin { diff --git a/templates/src/Rest/Rest_Controller.php b/templates/src/Rest/Rest_Controller.php index 806f566..aa90f83 100644 --- a/templates/src/Rest/Rest_Controller.php +++ b/templates/src/Rest/Rest_Controller.php @@ -60,6 +60,7 @@ public function register_routes() { }, ), ), + 'schema' => $this->get_public_item_schema( ... ), ) ); } @@ -91,4 +92,40 @@ public function get_items( $request ) { return rest_ensure_response( $data ); } + + /** + * Item schema, describing the shape of get_items()'s response. Backs the + * endpoint's OPTIONS response and any schema-driven API tooling (the + * block editor's data layer included) -- without it the endpoint isn't + * self-describing. + * + * @return array + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $this->schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => '{{PREFIX}}_item', + 'type' => 'object', + 'properties' => array( + 'message' => array( + 'description' => __( 'Response message.', '{{SLUG}}' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'param' => array( + 'description' => __( 'Echoed request parameter.', '{{SLUG}}' ), + 'type' => array( 'string', 'null' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $this->schema ); + } } diff --git a/templates/tests/Unit/Rest_Controller_Test.php b/templates/tests/Unit/Rest_Controller_Test.php index a6644fc..a133d81 100644 --- a/templates/tests/Unit/Rest_Controller_Test.php +++ b/templates/tests/Unit/Rest_Controller_Test.php @@ -64,4 +64,19 @@ public function test_permissions_check_gates_on_capability(): void { $this->assertFalse( $controller->get_items_permissions_check( $request ) ); } + + /** + * The item schema describes get_items()'s response shape, so the + * endpoint is self-describing over OPTIONS rather than an empty default. + */ + public function test_get_item_schema_describes_the_response_shape(): void { + Functions\when( '__' )->returnArg(); + + $controller = new Rest_Controller(); + $schema = $controller->get_item_schema(); + + $this->assertSame( 'object', $schema['type'] ); + $this->assertArrayHasKey( 'message', $schema['properties'] ); + $this->assertArrayHasKey( 'param', $schema['properties'] ); + } } diff --git a/templates/tests/bootstrap.php b/templates/tests/bootstrap.php index 0ff8b66..c257442 100644 --- a/templates/tests/bootstrap.php +++ b/templates/tests/bootstrap.php @@ -61,6 +61,44 @@ class WP_REST_Controller { * @var string */ protected $rest_base; + + /** + * The controller's cached item schema. + * + * @var array|null + */ + protected $schema; + + /** + * Stub: real core strips arg-only properties before exposing a schema + * over OPTIONS; there are none to strip in this minimal stub. + * + * @return array + */ + public function get_public_item_schema() { + return $this->get_item_schema(); + } + + /** + * Stub: real core merges in schema for fields added via + * register_rest_field(); none are registered in this stub. + * + * @param array $schema Item schema. + * @return array + */ + protected function add_additional_fields_schema( $schema ) { + return $schema; + } + + /** + * Stub default; a subclass implementing get_item_schema() overrides + * this. + * + * @return array + */ + public function get_item_schema() { + return array(); + } } }