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
63 changes: 50 additions & 13 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,8 @@ function scaffoldInto(answers, targetDir) {
// + Core\Uninstaller only ship when one of these modules persists state.
has_uninstall: ['admin_settings', 'cron', 'custom_table', 'elementor_widget'].some(m => selectedModules.includes(m)),
has_woo: hasAnyWoo,
// The only modules that write a templates/ directory (WC template overrides).
has_wc_template_overrides: hasWooEmail || hasWooMyAccount,
lint_wp_org: lintTarget === 'wp-org' || lintTarget === 'both',
lint_vip: needsVip
};
Expand Down Expand Up @@ -956,8 +958,8 @@ function scaffoldInto(answers, targetDir) {
writeTemplateFile(path.join(templatesDir, 'phpcs.xml'), 'phpcs.xml');
writeTemplateFile(path.join(templatesDir, 'tests/bootstrap.php'), 'tests/bootstrap.php');
writeTemplateFile(path.join(templatesDir, 'phpunit.xml.dist'), 'phpunit.xml.dist');
writeTemplateFile(path.join(templatesDir, 'tests/Unit/Plugin_TestCase.php'), 'tests/Unit/Plugin_TestCase.php');
writeTemplateFile(path.join(templatesDir, 'tests/Unit/Example_Test.php'), 'tests/Unit/Example_Test.php');
writeTemplateFile(path.join(templatesDir, 'tests/Unit/Services_Test.php'), 'tests/Unit/Services_Test.php');
writeTemplateFile(path.join(templatesDir, 'gitignore.tpl'), '.gitignore');
writeTemplateFile(path.join(templatesDir, 'editorconfig.tpl'), '.editorconfig');
writeTemplateFile(path.join(templatesDir, 'LICENSE'), 'LICENSE');
Expand Down Expand Up @@ -991,12 +993,15 @@ function scaffoldInto(answers, targetDir) {
const bootLines = [];
const wooBootLines = [];
const servicesAccessors = [];
const servicesAccessorTests = [];

// One memoised getter on Services: name() -> new <Type>(). $short is the
// class name relative to the plugin root namespace (Services lives there).
function servicesAccessor(name, short) {
// Register a shared-service getter: emits the Services::name() accessor
// ({{SERVICES_ACCESSORS}}) and a matching Services_Test method
// ({{SERVICES_ACCESSOR_TESTS}}). `short` is the class name relative to the
// plugin root namespace (Services lives there).
function addService(name, short) {
const type = short.split('\\').pop();
return [
servicesAccessors.push([
'\t/**',
`\t * Shared ${type} instance.`,
'\t *',
Expand All @@ -1006,7 +1011,28 @@ function scaffoldInto(answers, targetDir) {
`\t\treturn self::$instances['${name}'] ??= new ${short}();`,
'\t}',
'',
].join('\n');
].join('\n'));
servicesAccessorTests.push([
'\t/**',
`\t * Services::${name}() is a memoised singleton, overridable via set()/reset().`,
'\t *',
'\t * @return void',
'\t */',
`\tpublic function test_${name}_is_a_memoised_singleton(): void {`,
`\t\t$a = $this->createMock( \\{{NS}}\\${short}::class );`,
`\t\tServices::set( '${name}', $a );`,
'',
`\t\t$this->assertSame( $a, Services::${name}() );`,
`\t\t$this->assertSame( Services::${name}(), Services::${name}() );`,
'',
'\t\tServices::reset();',
`\t\t$b = $this->createMock( \\{{NS}}\\${short}::class );`,
`\t\tServices::set( '${name}', $b );`,
'',
`\t\t$this->assertSame( $b, Services::${name}(), 'reset() cleared the previous override' );`,
'\t}',
'',
].join('\n'));
}

if (selectedModules.includes('cli')) {
Expand All @@ -1023,7 +1049,7 @@ function scaffoldInto(answers, targetDir) {
writeTemplateFile(path.join(templatesDir, 'src/Admin/views/settings-page.php'), 'src/Admin/views/settings-page.php');

bootLines.push('\t\t( new Admin\\Settings_Registrar( Services::settings_repository() ) )->init_hooks();');
servicesAccessors.push(servicesAccessor('settings_repository', 'Admin\\Settings_Repository'));
addService('settings_repository', 'Admin\\Settings_Repository');
}
if (selectedModules.includes('shortcode')) {
writeTemplateFile(path.join(templatesDir, 'src/Frontend/Shortcode.php'), 'src/Frontend/Shortcode.php');
Expand Down Expand Up @@ -1057,15 +1083,15 @@ function scaffoldInto(answers, targetDir) {
if (selectedModules.includes('caching')) {
writeTemplateFile(path.join(templatesDir, 'src/Cache/Cache_Service.php'), 'src/Cache/Cache_Service.php');
writeTemplateFile(path.join(templatesDir, 'tests/Unit/Cache_Service_Test.php'), 'tests/Unit/Cache_Service_Test.php');
servicesAccessors.push(servicesAccessor('cache', 'Cache\\Cache_Service'));
addService('cache', 'Cache\\Cache_Service');
}
if (selectedModules.includes('custom_table')) {
writeTemplateFile(path.join(templatesDir, 'src/Database/Schema.php'), 'src/Database/Schema.php');
writeTemplateFile(path.join(templatesDir, 'src/Database/Item_Repository.php'), 'src/Database/Item_Repository.php');
writeTemplateFile(path.join(templatesDir, 'tests/Unit/Schema_Test.php'), 'tests/Unit/Schema_Test.php');
writeTemplateFile(path.join(templatesDir, 'tests/Unit/Item_Repository_Test.php'), 'tests/Unit/Item_Repository_Test.php');
bootLines.push('\t\t( new Database\\Schema() )->init_hooks();');
servicesAccessors.push(servicesAccessor('item_repository', 'Database\\Item_Repository'));
addService('item_repository', 'Database\\Item_Repository');
}
if (selectedModules.includes('elementor_widget')) {
if (selectedModules.includes('editor_config')) {
Expand Down Expand Up @@ -1124,29 +1150,29 @@ function scaffoldInto(answers, targetDir) {
writeTemplateFile(path.join(templatesDir, 'src/Woo/Orders/Order_Status_Service.php'), 'src/Woo/Orders/Order_Status_Service.php');
writeTemplateFile(path.join(templatesDir, 'tests/Unit/Order_Status_Service_Test.php'), 'tests/Unit/Order_Status_Service_Test.php');
wooBootLines.push('\t\t\t( new Woo\\Providers\\Order_Status_Provider( Services::order_status_service() ) )->init_hooks();');
servicesAccessors.push(servicesAccessor('order_status_service', 'Woo\\Orders\\Order_Status_Service'));
addService('order_status_service', 'Woo\\Orders\\Order_Status_Service');
}
if (hasWooActionScheduler) {
writeTemplateFile(path.join(templatesDir, 'src/Woo/Providers/Action_Scheduler_Provider.php'), 'src/Woo/Providers/Action_Scheduler_Provider.php');
writeTemplateFile(path.join(templatesDir, 'src/Woo/Tasks/Action_Scheduler_Service.php'), 'src/Woo/Tasks/Action_Scheduler_Service.php');
writeTemplateFile(path.join(templatesDir, 'tests/Unit/Action_Scheduler_Service_Test.php'), 'tests/Unit/Action_Scheduler_Service_Test.php');
wooBootLines.push('\t\t\t( new Woo\\Providers\\Action_Scheduler_Provider( Services::action_scheduler_service() ) )->init_hooks();');
servicesAccessors.push(servicesAccessor('action_scheduler_service', 'Woo\\Tasks\\Action_Scheduler_Service'));
addService('action_scheduler_service', 'Woo\\Tasks\\Action_Scheduler_Service');
}
if (hasWooStoreApi) {
writeTemplateFile(path.join(templatesDir, 'src/Woo/Providers/Store_Api_Provider.php'), 'src/Woo/Providers/Store_Api_Provider.php');
writeTemplateFile(path.join(templatesDir, 'src/Woo/Api/Store_Api_Extension.php'), 'src/Woo/Api/Store_Api_Extension.php');
writeTemplateFile(path.join(templatesDir, 'tests/Unit/Store_Api_Extension_Test.php'), 'tests/Unit/Store_Api_Extension_Test.php');
wooBootLines.push('\t\t\t( new Woo\\Providers\\Store_Api_Provider( Services::store_api_extension() ) )->init_hooks();');
servicesAccessors.push(servicesAccessor('store_api_extension', 'Woo\\Api\\Store_Api_Extension'));
addService('store_api_extension', 'Woo\\Api\\Store_Api_Extension');
}
if (hasWooMyAccount) {
writeTemplateFile(path.join(templatesDir, 'src/Woo/Providers/Account_Endpoint_Provider.php'), 'src/Woo/Providers/Account_Endpoint_Provider.php');
writeTemplateFile(path.join(templatesDir, 'src/Woo/Account/Account_Endpoint_Service.php'), 'src/Woo/Account/Account_Endpoint_Service.php');
writeTemplateFile(path.join(templatesDir, 'woo-account-templates/my-account/custom-endpoint.php'), `templates/my-account/${answers.prefix.toLowerCase()}-custom.php`);
writeTemplateFile(path.join(templatesDir, 'tests/Unit/Account_Endpoint_Service_Test.php'), 'tests/Unit/Account_Endpoint_Service_Test.php');
wooBootLines.push('\t\t\t( new Woo\\Providers\\Account_Endpoint_Provider( Services::account_endpoint_service() ) )->init_hooks();');
servicesAccessors.push(servicesAccessor('account_endpoint_service', 'Woo\\Account\\Account_Endpoint_Service'));
addService('account_endpoint_service', 'Woo\\Account\\Account_Endpoint_Service');
}
if (selectedModules.includes('interactivity')) {
writeTemplateFile(path.join(templatesDir, 'src/Frontend/Interactivity.php'), 'src/Frontend/Interactivity.php');
Expand Down Expand Up @@ -1376,6 +1402,17 @@ ${entries.join('\n')}
fs.mkdirSync(path.dirname(servicesDestPath), { recursive: true });
fs.writeFileSync(servicesDestPath, servicesContent, 'utf8');

// Services_Test.php only ships when there's at least one accessor to
// exercise — with none, the class has nothing behavioural to test.
if (servicesAccessorTests.length > 0) {
let servicesTest = fs.readFileSync(path.join(templatesDir, 'tests/Unit/Services_Test.php'), 'utf8');
servicesTest = servicesTest.replace('{{SERVICES_ACCESSOR_TESTS}}', () => servicesAccessorTests.join('\n'));
servicesTest = processTemplateContent(servicesTest, 'tests/Unit/Services_Test.php');
const servicesTestDest = path.join(targetDir, 'tests/Unit/Services_Test.php');
fs.mkdirSync(path.dirname(servicesTestDest), { recursive: true });
fs.writeFileSync(servicesTestDest, servicesTest, 'utf8');
}

// Single supported PHP line — see MIN_PHP. The matrix also runs the next
// minor so a scaffold surfaces forward-compat breakage early.
const ciPhpMatrix = "['8.2', '8.3', '8.4']";
Expand Down
2 changes: 1 addition & 1 deletion templates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ created:
{{#if needs_build_pipeline}}
npm install && npm run build
{{/if}}
composer install --no-dev --optimize-autoloader
composer prepare-dist # composer install --no-dev --optimize-autoloader
npm run plugin-zip
```

Expand Down
3 changes: 2 additions & 1 deletion templates/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
{{#if integration_tests}}
"test:integration": "phpunit -c phpunit-integration.xml.dist",
{{/if}}
"make-pot": "wp i18n make-pot . languages/{{SLUG}}.pot"
"make-pot": "wp i18n make-pot . languages/{{SLUG}}.pot",
"prepare-dist": "composer install --no-dev --optimize-autoloader"
},
"config": {
"allow-plugins": {
Expand Down
33 changes: 25 additions & 8 deletions templates/phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,23 @@
<!-- Only the block render.php files here match the "php" extension filter. -->
<file>./assets/src</file>
{{/if}}
{{#if has_wc_template_overrides}}
<!--
./templates/ holds WooCommerce email / My Account overrides. They're linted
for everything (docblocks, spacing, i18n, …) except the two sniffs below,
which flag things the override cannot avoid:
- PrefixAllGlobals: the overrides must call core WC hooks by their real,
unprefixed names (woocommerce_email_header, …) and receive loosely-named
locals ($first_name) from wc_get_template().
- EscapeOutput (plain-text bodies only): a plain-text email uses
wp_strip_all_tags(), which WPCS doesn't recognise as an escaper.
-->
<file>./templates</file>
{{/if}}

<exclude-pattern>/vendor/</exclude-pattern>
<exclude-pattern>/node_modules/</exclude-pattern>
<exclude-pattern>/assets/build/</exclude-pattern>
<!--
./templates/ (WooCommerce email + My Account overrides) is deliberately
not sniffed: those files run in WooCommerce's own context, call core WC
hooks like woocommerce_email_header, and echo values WooCommerce escapes
upstream — WPCS can only flag false positives there, and the directory
doesn't exist at all unless a woo:email / woo:my-account module is picked.
-->
<exclude-pattern>*/templates/*</exclude-pattern>

<arg value="sp"/>
<arg name="colors"/>
Expand Down Expand Up @@ -62,6 +67,9 @@

<rule ref="WordPress.NamingConventions.PrefixAllGlobals">
<exclude-pattern>/tests/</exclude-pattern>
{{#if has_wc_template_overrides}}
<exclude-pattern>*/templates/*</exclude-pattern>
{{/if}}
<properties>
<property name="prefixes" type="array">
<element value="{{PREFIX}}"/>
Expand All @@ -80,6 +88,15 @@
<rule ref="WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound">
<exclude-pattern>*/blocks/*/render.php</exclude-pattern>
</rule>
{{#if has_wc_template_overrides}}
<!--
Plain-text WooCommerce email bodies escape with wp_strip_all_tags(), which
WPCS doesn't know is an escaper. The HTML email template is still checked.
-->
<rule ref="WordPress.Security.EscapeOutput">
<exclude-pattern>*/templates/emails/plain/*</exclude-pattern>
</rule>
{{/if}}
<rule ref="WordPress.WP.I18n">
<properties>
<property name="text_domain" type="array">
Expand Down
10 changes: 4 additions & 6 deletions templates/src/CLI/Commands.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,14 @@ class Commands {
/**
* Register WP-CLI commands.
*
* The bootloader only instantiates this class behind a WP_CLI guard; the
* repeat check here keeps the class safe to call directly (e.g. in tests).
* Plugin::boot() only instantiates this class when WP_CLI is defined and
* truthy, so there is no constant check here — keeping it out means unit
* tests don't have to define( 'WP_CLI' ) (which they can't undo, and which
* leaks into every test that runs afterwards).
*
* @return void
*/
public function init_hooks(): void {
if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) {
return;
}

\WP_CLI::add_command( '{{PREFIX}} status', $this->status( ... ) );
\WP_CLI::add_command( '{{PREFIX}} cache clear', $this->cache_clear( ... ) );
}
Expand Down
3 changes: 1 addition & 2 deletions templates/tests/Unit/Account_Endpoint_Service_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@

use Brain\Monkey\Functions;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use PHPUnit\Framework\TestCase;
use {{NS}}\Woo\Account\Account_Endpoint_Service;

/**
* Class Account_Endpoint_Service_Test.
*/
class Account_Endpoint_Service_Test extends TestCase {
class Account_Endpoint_Service_Test extends Plugin_TestCase {

use MockeryPHPUnitIntegration;

Expand Down
3 changes: 1 addition & 2 deletions templates/tests/Unit/Action_Scheduler_Service_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@

use Brain\Monkey\Functions;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use PHPUnit\Framework\TestCase;
use {{NS}}\Woo\Tasks\Action_Scheduler_Service;

/**
* Class Action_Scheduler_Service_Test.
*/
class Action_Scheduler_Service_Test extends TestCase {
class Action_Scheduler_Service_Test extends Plugin_TestCase {

use MockeryPHPUnitIntegration;

Expand Down
3 changes: 1 addition & 2 deletions templates/tests/Unit/Ajax_Handler_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@

namespace {{NS}}\Tests\Unit;

use PHPUnit\Framework\TestCase;
use Brain\Monkey;
use Brain\Monkey\Functions;
use {{NS}}\Ajax\Ajax_Handler;

/**
* Class Ajax_Handler_Test.
*/
class Ajax_Handler_Test extends TestCase {
class Ajax_Handler_Test extends Plugin_TestCase {

/**
* Set up test environment.
Expand Down
3 changes: 1 addition & 2 deletions templates/tests/Unit/Block_Registrar_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

namespace {{NS}}\Tests\Unit;

use PHPUnit\Framework\TestCase;
use Brain\Monkey;
use Brain\Monkey\Actions;
use Brain\Monkey\Functions;
Expand All @@ -18,7 +17,7 @@
/**
* Class Block_Registrar_Test.
*/
class Block_Registrar_Test extends TestCase {
class Block_Registrar_Test extends Plugin_TestCase {

/**
* Set up test environment.
Expand Down
3 changes: 1 addition & 2 deletions templates/tests/Unit/Cache_Service_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@

namespace {{NS}}\Tests\Unit;

use PHPUnit\Framework\TestCase;
use Brain\Monkey;
use Brain\Monkey\Functions;
use {{NS}}\Cache\Cache_Service;

/**
* Class Cache_Service_Test.
*/
class Cache_Service_Test extends TestCase {
class Cache_Service_Test extends Plugin_TestCase {

/**
* Set up test environment.
Expand Down
3 changes: 1 addition & 2 deletions templates/tests/Unit/Cart_Summary_Block_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@

use Brain\Monkey\Functions;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use PHPUnit\Framework\TestCase;
use {{NS}}\Woo\Blocks\Cart_Summary_Block;

/**
* Class Cart_Summary_Block_Test.
*/
class Cart_Summary_Block_Test extends TestCase {
class Cart_Summary_Block_Test extends Plugin_TestCase {

use MockeryPHPUnitIntegration;

Expand Down
14 changes: 6 additions & 8 deletions templates/tests/Unit/Commands_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@

namespace {{NS}}\Tests\Unit;

use PHPUnit\Framework\TestCase;
use Brain\Monkey;
use Brain\Monkey\Functions;
use {{NS}}\CLI\Commands;

/**
* Class Commands_Test.
*/
class Commands_Test extends TestCase {
class Commands_Test extends Plugin_TestCase {

/**
* Set up test environment.
Expand All @@ -37,13 +36,12 @@ protected function tearDown(): void {
}

/**
* Both WP-CLI commands are registered by init_hooks() when WP_CLI is defined.
* Both WP-CLI commands are registered by init_hooks().
*
* No define( 'WP_CLI' ) here: Plugin::boot() owns that guard, so
* init_hooks() runs unconditionally and this test stays isolated.
*/
public function test_init_hooks_registers_commands_under_wp_cli(): void {
if ( ! defined( 'WP_CLI' ) ) {
define( 'WP_CLI', true );
}

public function test_init_hooks_registers_both_commands(): void {
( new Commands() )->init_hooks();

$this->assertArrayHasKey( '{{PREFIX}} status', \WP_CLI::$commands );
Expand Down
3 changes: 1 addition & 2 deletions templates/tests/Unit/Custom_Email_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@

use Brain\Monkey\Functions;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use PHPUnit\Framework\TestCase;
use {{NS}}\Woo\Emails\Custom_Email;

/**
* Class Custom_Email_Test.
*/
class Custom_Email_Test extends TestCase {
class Custom_Email_Test extends Plugin_TestCase {

use MockeryPHPUnitIntegration;

Expand Down
Loading
Loading