From 95a9d7c40913a50cd041dd603eadaa6987864b91 Mon Sep 17 00:00:00 2001 From: Akshat Date: Mon, 31 Aug 2026 12:51:33 +0530 Subject: [PATCH] fix(templates): guard the release zip; correct stale architecture docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit package.json - plugin-zip now runs `composer prepare-dist` first, so a zip is never built without the production autoloader (previously skipped silently, exit 0, when vendor/ was absent). - drop unbuilt assets/src/ from files[] — no runtime PHP reads it. New readme.txt "== Development ==" section points to the repo for source, gated on has_webpack_build, linking AUTHOR_URI when set. templates/README.md - "Architecture & Services" described a Core\Container / Service_Provider / Conditional / _providers filter system the generator has never produced. Replaced with a module-aware "Project structure" section. Releasing section updated for the chained plugin-zip. README.md (CLI's own) - same fictional architecture writeup corrected to the real singleton Plugin bootloader + plain init_hooks() modules; Releasing note updated. index.js - add has_author_uri template flag; point "Next steps" at packaging. tests - update the two plugin-zip assertions and the files[] assertion. Verified: 70/70 generator tests; scaffolds regenerated across build/no-build and woo/no-woo render clean with no stray tokens. --- README.md | 45 ++++++++++++++++++++++++----------------- index.js | 5 +++++ templates/README.md | 39 ++++++++++++++++++++++------------- templates/package.json | 5 ++--- templates/readme.txt | 15 ++++++++++++++ tests/generator.test.js | 7 ++++--- 6 files changed, 78 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 5fa4bec..085fffe 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # create-wp-plugin Interactive scaffold generator for modern, production-ready WordPress plugins — a -SOLID/DI architecture (Container + Service Providers), PSR-4 autoloading, -selectable WPCS/VIP coding standards, a Brain Monkey unit suite, a -`wp-scripts plugin-zip` distribution pipeline, and a set of opt-in modules +modular bootloader architecture (one `Plugin` composition root wiring plain +`init_hooks()` modules), PSR-4 autoloading, selectable WPCS/VIP coding standards, +a Brain Monkey unit suite, a `wp-scripts plugin-zip` distribution pipeline, and a +set of opt-in modules (REST, CPT, Cron, Caching, custom DB table, native Gutenberg blocks, Elementor, WooCommerce, the Interactivity API, WP-CLI commands, a real-WordPress integration suite, …) that stay freely combinable. @@ -61,7 +62,7 @@ interactive multiselect. Nothing here depends on anything else. | Module | What you get | | --- | --- | | `admin_settings` | Settings API page split into `Settings_Registrar` / `Settings_Repository` / a view | -| `shortcode` | A `Shortcode` provider | +| `shortcode` | A `Frontend\Shortcode` class registering one shortcode | | `rest_api` | A `WP_REST_Controller` subclass with a permission callback | | `ajax_handler` | Nonce + capability-guarded `admin-ajax` handler, plus the `assets/js/main.js` it enqueues | | `cpt_taxonomy` | `Post_Types` (CPT + taxonomy), wired into activation | @@ -71,7 +72,7 @@ interactive multiselect. Nothing here depends on anything else. | `elementor_widget` | `Widget_Registrar` auto-discovery of `src/Widgets/*`, convention-based CSS/JS | | `block` | Native Gutenberg block(s). Opens a sub-choice — `block:dynamic` (server-rendered via `render.php`) and/or `block:static` (`save()`-serialized). `block` / `block:all` = both. `Block_Registrar` globs `assets/build/blocks/*`, so adding more blocks later needs no PHP change | | `interactivity` | WordPress Interactivity API store (`view.js` + Script Module, WP 6.5+) | -| `woocommerce_hooks` | Opens a sub-choice of `woo:` components: `woo:gateway`, `woo:shipping`, `woo:email`, `woo:order-status`, `woo:product-type`, `woo:blocks`, `woo:action-scheduler`, `woo:store-api`, `woo:my-account`. `woocommerce` / `woo:all` = all. Each is its own `Service_Provider` that self-excludes when WooCommerce isn't active | +| `woocommerce_hooks` | Opens a sub-choice of `woo:` components: `woo:gateway`, `woo:shipping`, `woo:email`, `woo:order-status`, `woo:product-type`, `woo:blocks`, `woo:action-scheduler`, `woo:store-api`, `woo:my-account`. `woocommerce` / `woo:all` = all. Each is a `Woo\Providers\*` class wired inside a single `class_exists( 'WooCommerce' )` guard in `boot()` | | `cli` | `wp status` / `wp cache clear` (the latter iterates a `_cache_keys` filter — no module names another's cache keys) | | `editor_config` | `.vscode/` snippets, settings, recommended extensions | | `integration_tests` | `wp-phpunit` suite (`composer test:integration`), `phpunit-integration.xml.dist`, a boot test, the `.wp-env.json`, and the CI integration job | @@ -115,14 +116,18 @@ composer test:integration ### Releasing Every scaffold gets a `package.json` whose `files` field is the single source of -truth for what ships (there is no `.distignore`). Order matters: +truth for what ships (there is no `.distignore`). `npm run plugin-zip` runs +`composer prepare-dist` (`composer install --no-dev --optimize-autoloader`) +itself, so the production autoloader always exists before the archive is built: ```bash -npm install && npm run build # only if there's a JS pipeline -composer install --no-dev --optimize-autoloader -npm run plugin-zip # -> .zip, via @wordpress/scripts +npm install && npm run build # only if there's a JS pipeline — must run first +npm run plugin-zip # -> .zip, via @wordpress/scripts ``` +Unbuilt sources under `assets/src/` are not shipped; the generated `readme.txt` +points to the repository for them. + ### Adding another block `Block_Registrar` registers every `assets/build/blocks/*` directory that has a @@ -139,16 +144,20 @@ npm run build ## Architecture (generated plugin) -- `Plugin::create()` builds a `Core\Container` and a list of providers; - `Plugin::boot()` runs each one. Not a singleton — construct one directly with - fakes in a test. -- Providers implement `Contracts\Service_Provider`: `register()` for container - bindings only, `boot()` for WordPress hooks. -- `Contracts\Conditional::is_needed()` lets a provider self-exclude (every - WooCommerce provider skips itself when WooCommerce isn't installed). +- `Plugin` is a singleton bootloader. `Plugin::instance()->boot()` (fired on + `plugins_loaded`) runs once — a re-entry guard makes any later call a no-op. +- `boot()` is a flat list of `( new Some\Module() )->init_hooks();` lines, one per + selected module. A module is a plain class with an `init_hooks()` method that + registers its hooks — no base class, no interface, no auto-discovery. A class + runs only because `boot()` names it. +- WooCommerce modules live under `src/Woo/` and are wired inside a single + `class_exists( 'WooCommerce' )` guard in `boot()`, so the plugin is inert + without WooCommerce. Each `Woo\Providers\*` class is one such module. +- `Services` (generated only when a module needs a shared collaborator) is a + static locator of memoised singletons — `Services::set()` / `reset()` are test + seams. A module receives a service by constructor injection from `boot()`. - `Core\Activator` / `Deactivator` implement `Contracts\Activatable` / - `Deactivatable` and resolve dependencies from the container. -- Extend without touching core files via the `_providers` filter. + `Deactivatable` and run from the activation / deactivation hooks in the main file. - Cross-cutting cleanup goes through filters, not hard references: `cli` and the uninstaller iterate `_cache_keys`; modules that cache register their own keys. diff --git a/index.js b/index.js index c092d61..3b6544b 100644 --- a/index.js +++ b/index.js @@ -846,6 +846,9 @@ function scaffoldInto(answers, targetDir) { has_woo: hasAnyWoo, // The only modules that write a templates/ directory (WC template overrides). has_wc_template_overrides: hasWooEmail || hasWooMyAccount, + // readme.txt "== Development ==" links to source when a repo URL is known; + // falls back to a fill-in placeholder otherwise. + has_author_uri: Boolean(answers.authorUri && answers.authorUri.trim()), // Set from servicesAccessors after the module loop: when no module // registers an accessor, Services.php (and its reset() wiring in // Plugin_TestCase) is not generated. @@ -1754,6 +1757,8 @@ ${entries.join('\n')} console.log(' composer lint'); console.log(' composer test'); console.log(' git init && git add -A && git commit -m "scaffold"\n'); + console.log('To package a release zip, see "Releasing" in README.md'); + console.log(` ${templateFlags.has_webpack_build ? 'npm run build && ' : ''}npm run plugin-zip # plugin-zip runs "composer prepare-dist" itself\n`); console.log('Note: composer install may prompt to allow dealerdirect/phpcodesniffer-composer-installer — answer yes.'); console.log(' First run note: "No composer.lock file present" is normal; Composer will generate it automatically.\n'); } diff --git a/templates/README.md b/templates/README.md index 37da321..abf6a97 100644 --- a/templates/README.md +++ b/templates/README.md @@ -14,11 +14,19 @@ > Note: `assets/build` is gitignored and generated during build. {{/if}} -## Architecture & Services -This plugin uses a modular composition root: `Plugin::create()` builds a `{{NS}}\Core\Container` and a list of providers, then `Plugin::boot()` runs each one. -- Providers implement `{{NS}}\Contracts\Service_Provider` (`register()` for container bindings, `boot()` for WordPress hooks). -- A provider can optionally implement `{{NS}}\Contracts\Conditional` to self-exclude (e.g. only run when a required plugin is active). -- Additional providers can be injected without modifying core files using the `{{PREFIX}}_providers` WordPress filter. +## Project structure + +- `{{SLUG}}.php` — plugin entry point: headers, constants, autoloader, bootstrap. +- `src/` — PHP classes, PSR-4 autoloaded under the `{{NS}}\` namespace. +- `languages/` — translation files. +- `tests/` — automated test suites. +- `.github/workflows/` — CI (lint + tests). +{{#if needs_build_pipeline}} +- `assets/src/` — JS/CSS sources; `npm run build` compiles them into `assets/build/`. +{{/if}} +{{#if has_wc_template_overrides}} +- `templates/` — WooCommerce template overrides. +{{/if}} {{#if elementor_widget}} ## Elementor Widgets Convention @@ -74,19 +82,22 @@ The bundled starters: `example` (dynamic, `render.php`) and/or `example-static` ## Releasing -Build a distributable zip with `@wordpress/scripts`. The order matters — the -production autoloader and built assets have to exist *before* the archive is -created: - ```sh {{#if needs_build_pipeline}} npm install && npm run build {{/if}} -composer prepare-dist # composer install --no-dev --optimize-autoloader npm run plugin-zip ``` -`plugin-zip` archives exactly the paths in package.json's `files` field -(`vendor/` and `assets/src/` included, per WordPress.org guidelines) into -`{{SLUG}}.zip`. There is no `.distignore` — `files` is the single source of -truth. Re-run `composer install` afterwards to restore your dev dependencies. +`npm run plugin-zip` runs `composer prepare-dist` (`composer install --no-dev +--optimize-autoloader`) first, then packages the paths listed in `package.json`'s +`files` field — including a production `vendor/` — into `{{SLUG}}.zip`. There is +no `.distignore`; `files` is the single source of truth. +{{#if needs_build_pipeline}} + +Run `npm run build` first — compiled assets must exist before packaging. Unbuilt +sources under `assets/src/` are not shipped; `readme.txt` points to the +repository for them. +{{/if}} + +Afterwards, run `composer install` to restore your dev dependencies. diff --git a/templates/package.json b/templates/package.json index c62f946..57ba9d2 100644 --- a/templates/package.json +++ b/templates/package.json @@ -20,11 +20,10 @@ "templates", "assets/build", "assets/css", - "assets/js", - "assets/src" + "assets/js" ], "scripts": { - "plugin-zip": "wp-scripts plugin-zip", + "plugin-zip": "composer prepare-dist && wp-scripts plugin-zip", "lint:js": "wp-scripts lint-js assets/src tests/js --no-error-on-unmatched-pattern", "lint:style": "wp-scripts lint-style \"assets/src/**/*.{css,scss}\" --allow-empty-input"{{#if has_webpack_build}}, "build": "wp-scripts build --webpack-src-dir=assets/src --output-path=assets/build", diff --git a/templates/readme.txt b/templates/readme.txt index e98937d..04bcedb 100644 --- a/templates/readme.txt +++ b/templates/readme.txt @@ -19,6 +19,21 @@ License URI: https://www.gnu.org/licenses/gpl-2.0.html 1. Upload the plugin files to the `/wp-content/plugins/{{SLUG}}` directory, or install the plugin through the WordPress plugins screen directly. 2. Activate the plugin through the 'Plugins' screen in WordPress. +{{#if has_webpack_build}} +== Development == + +The JavaScript in this plugin is compiled. The complete, human-readable source +for every built asset lives in the `assets/src/` directory of the project's +public repository, alongside the build tooling (package.json, webpack config) +used to produce the files shipped in this package: + +{{#if has_author_uri}} +{{AUTHOR_URI}} +{{else}} +(add your public repository URL here) +{{/if}} + +{{/if}} == Frequently Asked Questions == = How do I configure this plugin? = diff --git a/tests/generator.test.js b/tests/generator.test.js index f4a50f0..281b2aa 100644 --- a/tests/generator.test.js +++ b/tests/generator.test.js @@ -978,9 +978,10 @@ test('pure-PHP scaffold gets a packaging-only package.json — no build pipeline // B6.14: package.json always ships for `npm run plugin-zip` + JS/CSS lint... const pkg = JSON.parse(fs.readFileSync(path.join(outDir, 'package.json'), 'utf8')); assert.equal(pkg.private, true); - assert.equal(pkg.scripts['plugin-zip'], 'wp-scripts plugin-zip'); + assert.equal(pkg.scripts['plugin-zip'], 'composer prepare-dist && wp-scripts plugin-zip'); assert.ok(pkg.scripts['lint:js'] && pkg.scripts['lint:style']); - assert.ok(Array.isArray(pkg.files) && pkg.files.includes('vendor') && pkg.files.includes('assets/src'), 'B6.14a: vendor/ and assets/src/ ship in the zip'); + assert.ok(Array.isArray(pkg.files) && pkg.files.includes('vendor'), 'B6.14a: vendor/ ships in the zip'); + assert.ok(!pkg.files.includes('assets/src'), 'B6.14a: unbuilt assets/src/ does not ship'); // ...but nothing build-pipeline-ish. assert.equal(pkg.scripts.build, undefined); assert.equal(pkg.scripts.start, undefined); @@ -1337,7 +1338,7 @@ test('WooCommerce granular sub-modules: pure-PHP (e.g. woo:shipping + woo:email) const pkg = JSON.parse(fs.readFileSync(path.join(outDir, 'package.json'), 'utf8')); assert.equal(pkg.scripts.build, undefined, 'pure-PHP WooCommerce gets no build script'); - assert.equal(pkg.scripts['plugin-zip'], 'wp-scripts plugin-zip', 'but still the packaging script'); + assert.equal(pkg.scripts['plugin-zip'], 'composer prepare-dist && wp-scripts plugin-zip', 'but still the packaging script'); assert.ok(!fs.existsSync(path.join(outDir, 'webpack.config.js')), 'pure-PHP WooCommerce must not emit webpack.config.js'); assert.ok(!fs.existsSync(path.join(outDir, 'src/Woo/Gateways/Gateway.php')), 'unselected sub-module should not exist');