Generated-plugin hygiene + module-system expansion (Phases 1–6) - #9
Merged
Conversation
…ead of wp_footer (NEW-2)
…x and numeric namespace suggestions (NEW-1, NEW-22, NEW-23)
…plate replacements (NEW-14, NEW-15)
…remove dead main entry in package.json (NEW-24, NEW-26, NEW-27)
- Add WooCommerce modules: Account endpoint, Store API extension, Order status service, Action Scheduler task service, and their DI providers - Add WP account/email templates and .wp-env.json for local dev - Expand test suite: new Unit tests for Woo services, widgets, REST, schema, repositories, shortcodes; JS view test; richer bootstrap - Various fixes to Ajax handler, Elementor widget registrar, sample widget, custom email, and gateway templates - Update generator, verify script, phpcs config, CI workflow, and docs
The uncommitted WIP widened the webpack.config.js write guard to `if (needsBuildPipeline)`, so a `--react`-only scaffold (single `./assets/src/index.js` entry that wp-scripts auto-detects via `--webpack-src-dir`) started emitting a redundant override and broke the "single default entry needs no webpack.config.js override" guard test. Restore the narrow condition: only Interactivity or a WooCommerce JS pipeline (view.js / gateway / blocks / block.json) needs the override. npm test: 42/42.
applyConditionals() gives the engine {{#if flag}} / {{#unless flag}} /
{{else}} (nesting + standalone-line trimming), driven by a templateFlags
table. It runs before token substitution, so {{TOKEN}}s inside a kept
block still resolve. An unbalanced template throws instead of shipping a
literal tag.
Nine variation-only tokens are gone — templates now carry their own
optional sections:
REACT_ADMIN_ROOT, REACT_ADMIN_HOOK_GUARD, REACT_ASSETS_REGISTRATION,
PHPCS_RULESETS, VSCODE_EXTRA_STUB_PATH, README_ELEMENTOR_DOCS,
README_REACT_INSTALL, README_REACT_SCRIPTS, README_CLI_DOCS
and their bespoke .replace() call sites collapse into plain
writeTemplateFile() calls.
Generator-built tokens (PROVIDER_REGISTRATIONS, *_BODY, CI_NODE_JOB,
CI_PHP_MATRIX, PLUGIN_HEADER_EXTRA, WOOCOMMERCE_HPOS,
COMPOSER_EXTRA_REQUIRE_DEV, PACKAGE_EXTRA_*) stay as-is: data-driven
codegen, not on/off toggles. A new module extends an accumulator there,
never a new token.
B2.3 marked closed (Elementor PHP moved to template files in v2).
Tests: 55/55 (13 new in tests/engine.test.js). Generated at zero /
admin_settings+elementor / full+react+VIP; php -l clean, phpcs.xml
XML-balanced, no stray tags.…ilter
CLI\Commands::cache_clear() hardcoded delete_transient(
'{prefix}_elementor_widgets' ) — generated even in builds without the
Elementor module — and buried the transient purge in an else branch so
plain-MySQL installs kept stale rows (NEW-42).
Now it flushes the plugin's object-cache group (when a persistent cache
exists) and, unconditionally, iterates apply_filters( '{prefix}_cache_keys',
[] ) deleting each transient. Modules that cache declare their own keys:
Elementor\Widget_Registrar::register_cache_keys() adds
'{prefix}_elementor_widgets' via the filter in boot(). uninstall.php runs
unbooted (no filter listeners), so its explicit delete_transient for that
key is emitted only in the elementor_widget branch of the generator, and
the hardcoded line is gone from the Uninstaller template.
No module names another module's internals now. Tests: 56/56 (+1).`.vscode/php.code-snippets`, `.vscode/extensions.json` and
`.vscode/settings.json` were written into every scaffold. They are now
gated behind a new `editor_config` module; `.vscode/php-elementor.code-snippets`
needs both `editor_config` and `elementor_widget`. A zero-module (or
non-editor_config) scaffold no longer has a `.vscode/` directory.
Also replaced the fragile `MODULE_DEFINITIONS.slice(0, 11)` in the
interactive prompt with a `!startsWith('woo:')` filter so newly added
modules show up without a magic count. Tests: 57/57 (+1).`src/CLI/Commands.php` and its `new CLI\Commands()` wiring in
Plugin::create() were emitted into every scaffold. Both are now gated
behind a new `cli` module: the file ships only when selected, and the
`if ( defined( 'WP_CLI' ) && WP_CLI )` block in Plugin.php is a
{{#if cli}} template block. A zero-module scaffold has no CLI code and
Plugin::create() never mentions WP_CLI.
B6.17: Commands.php dropped its top-level `if ( ! WP_CLI ) { return; }` —
that stopped the class from being declared and broke PSR-4 autoloading /
unit tests. The guard now lives inside boot(). Adds tests/Unit/Commands_Test.php
(boot registration + cache_clear filter behaviour) and a WP_CLI stub in
the unit bootstrap. Tests: 58/58 (+2).assets/js/main.js was written into every scaffold but only ever enqueued by the ajax_handler module's front-end script, and assets/css/main.css was an empty stub nothing enqueued at all. main.js now ships only with ajax_handler; main.css is deleted. A scaffold with neither ajax_handler nor elementor_widget has no assets/ tree. Tests: 59/59 (+1).
Rather than a toggle: uninstall.php and Core\Uninstaller now ship only
when a selected module persists cleanup-worthy state (an option from
admin_settings, a table from custom_table, a scheduled event from cron, a
transient from elementor_widget) — i.e. when uninstallLines is non-empty.
A zero-module or purely presentational scaffold (shortcode / rest_api)
persists nothing but the {prefix}_version marker and ships neither file.
When it does ship, Uninstaller::cleanup() still deletes {prefix}_version
alongside the per-module lines.
Rationale for deriving over asking: there is no coherent plugin that
persists data via a module but wants to leave it behind on delete
(wp.org review flags orphaned data), and asking is one more prompt for a
choice that has one sensible answer. Tests: 60/60 (+1).…s module
tests/bootstrap-integration.php, phpunit-integration.xml.dist,
tests/Integration/Plugin_Boot_Test.php, the wp-phpunit + phpunit-polyfills
dev deps, the composer `test:integration` script, the CI `integration`
job, and the integration line in README are all gated behind a new
`integration_tests` module (composer/ci/README use {{#if integration_tests}}
blocks). .wp-env.json now ships when integration_tests OR a JS build
pipeline (Playwright) is present, instead of always.
A zero/light scaffold no longer carries a wp-phpunit dev dependency or a
WordPress test bootstrap it never runs. Also refreshed the --modules help
string with cli / editor_config / integration_tests. Tests: 60/60.A failure partway through runGenerator() (disk full, permission error, antivirus lock) left an orphaned half-written plugin directory behind. The write phase is now wrapped: on any error the directory is removed — but only if runGenerator() created it, never a directory that already existed when the user pointed us at it. Body extracted verbatim into an internal scaffoldInto() so the error boundary needs no reindent. Tests: 61/61 (+1).
Piping into the CLI (or running it in CI) without --yes previously hung forever on the first interactive prompt waiting for stdin that never arrives. `main()` now checks `process.stdin.isTTY` before entering the prompt flow and exits 1 with a copy-pasteable non-interactive example. --yes through a pipe is unaffected.
…nals) The ❌ / 🚀 / ✅ markers rendered as mojibake in the legacy Windows console and in minimal CI log viewers. All CLI-owned console output now goes through icon(glyph, ascii): emoji only when stdout is a TTY, NO_COLOR is unset, and — on Windows — a modern terminal is detected (WT_SESSION / TERM_PROGRAM / ConEmuTask / TERM); otherwise [x] / >> / [ok].
Only --yes mode re-validated the assembled answers; interactive mode relied solely on per-prompt validators, which run before the WooCommerce sub-module merge and can't do batch/cross-field checks. main() now runs validateAll() on the collected interactive answers before the summary, exiting 1 on failure instead of proceeding into a confusing generator error.
scripts/verify.js replaces the bash-only verify.sh: same five fixtures (minimal / elementor / woo / vip / full, now including the cli, editor_config and integration_tests modules), same per-fixture checks (php -l, composer install with retry, composer lint, composer test, unreplaced-token and TODO: SECURITY scans), same collect-failures-then- exit-1 behaviour — but it runs on native Windows too. Wired up as `npm run verify`; verify.sh deleted.
Windows installs the CLI bin as a generated .cmd/.ps1 shim rather than a symlink — a distinct path through index.js's isRunAsScript() main-guard that this project has regressed before. Added windows-latest to the self-consumption matrix, pinned `shell: bash` for the job so the POSIX steps run unchanged on the Windows runner's Git Bash, and taught the global-install step that Windows drops the shim in <prefix> not <prefix>/bin.
verify.sh was never wired into CI, so nothing caught regressions it would have. The ported scripts/verify.js now runs as a `verify` job (the fixture matrix: minimal / elementor / woo / vip / full, each php -l + composer lint + composer test + token scan). It overlaps the self-consumption job only in that both run composer on generated output — self-consumption proves the packaged bin installs and runs per-OS, verify covers the module combinations.
The CI matrix only ever ran Node 20 and 22, so the >=18 floor was an untested claim — and Node 18 reached end-of-life in April 2025. Aligning `engines` with what's actually verified.
composer.json (and the plugin header) declare GPL-2.0-or-later but no LICENSE file was scaffolded — a WordPress.org submission requirement and a plain open-source hygiene gap. The generator now always writes the GPLv2 text as LICENSE. Tests: 62/62 (+1).
`Tested up to: 6.7` was frozen in the template. It's now
{{TESTED_UP_TO}}, seeded to a recent stable floor (6.8, or the plugin's
own required WP version if that's higher) for the developer to bump each
release.…ing rejects
`Contributors: {{AUTHOR}}` put a display name (spaces, capitals) where
WordPress.org expects a user login — an automatic review rejection.
`Tags: wordpress, plugin, scaffolding` are exactly the generic terms
review flags. Now:
Contributors -> {{CONTRIBUTOR}}: the author name reduced to a
login-shaped slug (lowercase alphanumeric), or "yourusername".
Tags -> {{TAGS}}: up to 5 specific tags derived from the selected
modules (woocommerce, elementor, custom post type, ...), falling
back to distinctive words from the plugin name.…ge.json The scaffolded package.json had only name/version/description/scripts/ devDependencies — no "private": true (it's a plugin, never published to npm), no license (composer.json declares GPL-2.0-or-later), no author, no engines. All four added.
The scaffolded build pipeline pinned @wordpress/scripts at ^27.0.0, years behind. Bumped to ^30.0.0, with the packages that move in lockstep with it: @wordpress/jest-preset-default ^20 -> ^21, @wordpress/e2e-test-utils-playwright ^1.4 -> ^1.13.
The cpt_taxonomy Activator/Deactivator emitted `flush_rewrite_rules( false ); // phpcs:ignore WordPressVIPMinimum...` unconditionally — a real VIP restriction papered over with an ignore. Now: under a wp-org target the soft flush stays but the ignore comment is gone (WordPress-Extra doesn't restrict it, so it lints clean); under a vip / both target the call is dropped entirely with a comment pointing the developer at a Settings > Permalinks re-save or their deploy. Tests: 62/62 (+1).
Every scaffold now gets a package.json whose job — even for a pure-PHP
plugin — is packaging and asset linting:
- "plugin-zip": "wp-scripts plugin-zip" builds {slug}.zip
- a `files` whitelist replaces .distignore as the single source of
truth for zip contents. It includes vendor/ (the production
autoloader — excluding it fatals on activation, B6.14a) and
assets/src/ (source for the built JS, WordPress.org guideline #2,
B6.14a).
- "lint:js" / "lint:style" (wp-scripts lint-js / lint-style), wired
into CI too (B6.14c): the existing node-build job for pipeline
scaffolds, a new lint-assets job when there are hand-written widget /
AJAX assets but no build.
- build/start layer on only when there's a JS pipeline.
.distignore and templates/react/package.json are deleted; the unified
templates/package.json uses {{#if needs_build_pipeline}}.
README gains a Releasing section (B6.14b): build (if pipeline) ->
composer install --no-dev --optimize-autoloader -> npm run plugin-zip,
in that order. No wp-cli/dist-archive-command. Tests: 62/62.… static)
A native block previously only appeared if you selected WooCommerce. The
new `block` module scaffolds TWO worked examples so both patterns are on
the table:
assets/src/blocks/example/ dynamic block — save: () => null,
block.json (apiVersion 3, "{slug}/example", render: file:./render.php)
index.js / edit.js (RichText on one attr) / render.php (server output
via wp_kses_post + get_block_wrapper_attributes)
assets/src/blocks/example-static/ static block — save() serializes
block.json (no render field) / index.js / edit.js / save.js
(RichText.Content, useBlockProps.save)
src/Blocks/Block_Registrar.php Service_Provider; on `init`,
register_block_type() for each built dir in self::BLOCKS (no-op until
`npm run build` has produced the metadata)
tests/Unit/Block_Registrar_Test.php Brain Monkey: init hook + unbuilt no-op
tests/js/block.test.js Jest: dynamic block, null save
tests/js/block-static.test.js Jest: static block, save returns markup
Wiring: adds `block` to MODULE_DEFINITIONS; flips needsBuildPipeline and
pulls in the Jest setup (new `wantsJest` = react || interactivity ||
block); bumps required WP to 6.3 (block.json apiVersion 3); a block-only
build needs no webpack.config.js (wp-scripts finds block.json itself),
but block + --react does — the entry-override guard now covers it.
Freely combinable, references no other module. Also added `block` and the
Phase-4 modules to verify.js's full fixture and the CI self-consumption
run. Tests: 64/64.Cache_Service::set()/delete() wrote to *both* wp_cache_* and a transient. But set_transient() already stores into the object cache when a Redis / Memcached drop-in is active, so on such a site every value was written twice into the same backend. It now branches on wp_using_ext_object_cache(): the persistent object cache when there is one, transients (options table) otherwise — which is what "object cache + transient fallback" was always meant to be. get() mirrors it. Tests updated to cover both branches.
…overy
Two follow-ups on the block module:
1. Static vs dynamic is now a choice, mirroring the WooCommerce
sub-module pattern:
- interactive: picking "native Gutenberg block" opens a "12b. Select
block type(s)" multiselect (Dynamic default).
- CLI: --modules "block:dynamic" / "block:static" / "block:all";
plain "block" (and "block:all") expand to both.
Each variant scaffolds only its own folder + Jest test.
2. Block_Registrar no longer hardcodes a block list — it globs
assets/build/blocks/* for directories with a block.json and registers
each. Adding another block later needs zero PHP:
npx @wordpress/create-block <name> --no-plugin --target-dir assets/src/blocks/<name>
npm run build
(glob() carries a scoped phpcs:ignore for the VIP sniff — it's the
plugin's own build output, not user input.) A `{{#if block}}` README
section documents the workflow. Tests: 66/66 (+2).Brings the top-level README in line with everything on this branch: a full module table (block + block:dynamic/block:static, cli, editor_config, integration_tests, the woo: sub-modules), the derived uninstall.php rule, the wp-scripts plugin-zip release flow (no .distignore), "adding another block" via @wordpress/create-block --no-plugin, the caching backend-picks-one behaviour, the <prefix>_cache_keys filter, non-TTY / NO_COLOR / rollback behaviour, `npm run verify`, and Node >= 20. Drops the stale "use npx @wordpress/create-block" note (there's a block module now).
Two things broke `composer lint` on generated scaffolds in CI: 1. `<file>./templates</file>` in the generated phpcs.xml (added in an earlier WIP commit). That directory only exists when a woo:email / woo:my-account module is picked, so PHPCS aborted with "file does not exist" (exit 3) for every other scaffold — and where it *did* exist, its WooCommerce email/My-Account override templates legitimately call core WC hooks (woocommerce_email_header, …) and echo values WC escapes upstream, which WPCS can only false-positive on. Dropped from the <file> list, with an <exclude-pattern> and a comment. 2. Method docblock short descriptions in Commands_Test / Block_Registrar_Test / Cache_Service_Test started with a lowercased method name (`boot() …`, `get() …`) → Generic.Commenting.DocComment.ShortNotCapital. Reworded to start with a capital. Also renamed the WP_CLI stub's `$callable` param (reserved-keyword warning). Tests: 66/66.
…e.json B6.9's version bumps were guesses and npm rejected @wordpress/jest-preset-default@^21 (latest is 14.x). Corrected to the current published set: @wordpress/scripts ^30 -> ^34 @wordpress/jest-preset-default ^21 -> ^14 @wordpress/e2e-test-utils-playwright ^1.13 -> ^1.54 Verified with `npm install --dry-run` on a react+interactivity+block scaffold — resolves clean (1542 packages, no ETARGET). Also refreshed a stale Block_Registrar comment. Tests: 66/66.
…ated
Same class as the ./templates fix: 0.7 made uninstall.php conditional
(derived from modules that persist state), but phpcs.xml still hardcoded
<file>./uninstall.php</file>, so `composer lint` aborted with "file does
not exist" (exit 3) for a minimal or woo-only scaffold. Wrapped it in
{{#if has_uninstall}} — a templateFlags entry kept in sync with the
uninstallLines.push() branches. Verified across all five verify.js
fixtures. Tests: 66/66.Uh oh!
There was an error while loading. Please reload this page.
akshat009 added a commit
that referenced
this pull request
Aug 29, 2026
BREAKING: --min-php is gone and the interactive "Minimum PHP version" prompt (#9) is removed. Every scaffold now requires PHP 8.3 — a single `export const MIN_PHP = '8.3'`, not configurable. validateMinPhp, requiredPhpFor and MODULE_PHP_FLOOR are deleted; the generated PHPUnit matrix is a fixed ['8.3', '8.4']; {{MIN_PHP}} resolves to 8.3 in the plugin header, composer.json, readme.txt, ci.yml and phpcs.xml (testVersion 8.3-). Templates modernised to that baseline (unconditional — no {{#if}} forks): - Constructor property promotion + readonly: Plugin (container/providers) and the four Woo\Providers\*_Provider service-override constructors lose their property declarations and assignment bodies. - Container::get(): mixed is unconditional again (the php_8_0 gate is gone). - First-class callable syntax: all 38 `array( $this, 'method' )` and the 3 `array( Class::class, 'method' )` hook/command callbacks become `$this->method(...)` / `Class::method(...)`. match()/enum were considered but have no natural home — no switch statements, no related-constant clusters. Skipped rather than forced. Verified: 67 generator + 13 engine tests; full-module and minimal 8.3 scaffolds — php -l clean, {{MIN_PHP}} -> 8.3 everywhere, no stray tags. composer lint / PHPCS on the modern output is exercised by the self-consumption + verify CI jobs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Works the audit backlog through Phases 1–6. 38 commits, one backlog item per commit,
npm testgreen at every step (66/66, up from 24 — 42 generator + 13 engine + 11 new).Branch was reset off
origin/main(00f9359); no merge conflicts.Phase 1 — stop shipping code that runs on its own
assets/js/main.jsAJAX demo wired to a.{slug}-ajax-triggerclick, no top-level calladd_shortcode, not an unconditionalwp_footerhookconsole.logremoved from shipped JSwp_ajax_nopriv_is opt-in ($allow_nopriv = false)Cron\Scheduler::execute_cron_job()has a minimal real bodyPhase 2 — make the tests real
*_Test.php(Brain Monkey, container-injected fakes)tests/js/view.test.jsfor the Interactivity moduletest:js+test:e2e+ JS/CSS-lint suites (previously configured, never run).wp-env.jsonships withintegration_testsor a JS pipeline@wordpress/jest-preset-defaultis an explicit devDependencyPhase 3 — template-engine conditionals (B2.4)
applyConditionals()adds{{#if}}/{{#unless}}/{{else}}(nesting + standalone-line trimming). Nine variation-only tokens removed; their bespoke.replace()sites collapse into plainwriteTemplateFile(). Newtests/engine.test.js. B2.1 / B2.2 / B2.5 (restructuringindex.js) intentionally left out of scope.Phase 4 — right-size the baseline
Zero-module scaffold: 33 → 22 files.
climodule (+ B6.17 — dropped the top-levelreturnthat broke PSR-4)uninstall.php+Core\Uninstallerderived from modules that persist stateintegration_testsmodule (wp-phpunit suite, deps,test:integration, CI job)main.jsfolded intoajax_handler; deadmain.cssdeletededitor_configmodule owns all.vscode/*{prefix}_cache_keysfilter — no module names another's keyswoocommerce_hooks→ 9woo:sub-modules with a secondary multiselectPhase 5 — CLI & wp.org-compliance bugs
validateAll()in interactive mode · B1.7 fail fast with no TTY · B1.8 ASCII fallback for emoji (NO_COLOR/ dumb terminals)npm run verifyas a CI job · B3.3windows-latestin the self-consumption matrix · B3.4verify.sh→ cross-platformscripts/verify.js· B3.5engines→node >=20Tested up totokenised · B6.3 derivedContributors/Tags· B6.4LICENSEfile · B6.9@wordpress/scripts^27→^30· B6.11package.jsonprivate/license/author/engineswp-scripts plugin-zippipeline:fileswhitelist (incl.vendor/+assets/src/),lint:js/lint:stylein scripts + CI, README release section;.distignoredeletedflush_rewrite_rules()— soft flush unsuppressed on wp-org, dropped entirely on VIPPhase 6 — native Gutenberg
blockmoduleThe biggest capability gap (a native block previously needed WooCommerce). Static/dynamic is a sub-choice (
block:dynamic/block:static/block= both), mirroring the woo pattern.Block_Registrarglobsassets/build/blocks/*, so adding more blocks later needs zero PHP (npx @wordpress/create-block <name> --no-plugin …).Also
cachingreworked: one backend chosen at call time (object cache or transient), never bothwebpack.config.jsonly emitted when an entry override is genuinely neededREADME.mdrewritten: module matrix, architecture notes, releasing, adding blocks, all run methods (npm create/npx/ GitHub / clone)Verify
npm test→ 66/66--react: every generated*.phppassesphp -l, all JSON valid, no stray template tagsscripts/verify.js'scomposer lint/composer testlegs run in the new CIverifyjob (Windows AV flakiness blocks them locally)Out of scope (tracked, not in this PR)
CLI structural refactor (B2.1/B2.2/B2.5, A2 remainder); B5.x flags (
--force/--dry-run/--preset/--template/…); B6.12 deprecatedphpunit.xml.distattrs; B6.22/B6.23; roadmap modulesrequirements/migrations/assets/ etc.; the--architecture/--tests/--targetaxes.