Skip to content

Generated-plugin hygiene + module-system expansion (Phases 1–6) - #9

Merged
akshat009 merged 41 commits into
mainfrom
fix/improvements-and-bugfixes
Aug 29, 2026
Merged

Generated-plugin hygiene + module-system expansion (Phases 1–6)#9
akshat009 merged 41 commits into
mainfrom
fix/improvements-and-bugfixes

Conversation

@akshat009

Copy link
Copy Markdown
Owner

Works the audit backlog through Phases 1–6. 38 commits, one backlog item per commit, npm test green 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

  • 0.11assets/js/main.js AJAX demo wired to a .{slug}-ajax-trigger click, no top-level call
  • 0.12 Interactivity demo renders via add_shortcode, not an unconditional wp_footer hook
  • 0.13console.log removed from shipped JS
  • 0.14wp_ajax_nopriv_ is opt-in ($allow_nopriv = false)
  • 0.15Cron\Scheduler::execute_cron_job() has a minimal real body

Phase 2 — make the tests real

  • 0.1 / 0.2 every module that emits a class now emits its *_Test.php (Brain Monkey, container-injected fakes)
  • 0.3tests/js/view.test.js for the Interactivity module
  • 0.16 generated CI runs the integration + test:js + test:e2e + JS/CSS-lint suites (previously configured, never run)
  • 0.17.wp-env.json ships with integration_tests or a JS pipeline
  • 0.18@wordpress/jest-preset-default is an explicit devDependency

Phase 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 plain writeTemplateFile(). New tests/engine.test.js. B2.1 / B2.2 / B2.5 (restructuring index.js) intentionally left out of scope.

Phase 4 — right-size the baseline

Zero-module scaffold: 33 → 22 files.

  • 0.6cli module (+ B6.17 — dropped the top-level return that broke PSR-4)
  • 0.7uninstall.php + Core\Uninstallerderived from modules that persist state
  • 0.8integration_tests module (wp-phpunit suite, deps, test:integration, CI job)
  • 0.9main.js folded into ajax_handler; dead main.css deleted
  • 0.10editor_config module owns all .vscode/*
  • B6.16 cache cleanup goes through a {prefix}_cache_keys filter — no module names another's keys
  • F6woocommerce_hooks → 9 woo: sub-modules with a secondary multiselect

Phase 5 — CLI & wp.org-compliance bugs

  • B1.3 rollback of a half-written scaffold · B1.4validateAll() in interactive mode · B1.7 fail fast with no TTY · B1.8 ASCII fallback for emoji (NO_COLOR / dumb terminals)
  • B3.2npm run verify as a CI job · B3.3windows-latest in the self-consumption matrix · B3.4verify.sh → cross-platform scripts/verify.js · B3.5enginesnode >=20
  • B6.2Tested up to tokenised · B6.3 derived Contributors / Tags · B6.4LICENSE file · B6.9@wordpress/scripts^27^30 · B6.11package.jsonprivate/license/author/engines
  • B6.14 (+a/b/c)wp-scripts plugin-zip pipeline: files whitelist (incl. vendor/ + assets/src/), lint:js/lint:style in scripts + CI, README release section; .distignore deleted
  • B6.19flush_rewrite_rules() — soft flush unsuppressed on wp-org, dropped entirely on VIP

Phase 6 — native Gutenberg block module

The 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

  • caching reworked: one backend chosen at call time (object cache or transient), never both
  • webpack.config.js only emitted when an entry override is genuinely needed
  • CLI README.md rewritten: module matrix, architecture notes, releasing, adding blocks, all run methods (npm create / npx / GitHub / clone)

Verify

  • npm test → 66/66
  • Regenerated at zero / small combos / all-modules --react: every generated *.php passes php -l, all JSON valid, no stray template tags
  • scripts/verify.js's composer lint / composer test legs run in the new CI verify job (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 deprecated phpunit.xml.dist attrs; B6.22/B6.23; roadmap modules requirements / migrations / assets / etc.; the --architecture / --tests / --target axes.

…x and numeric namespace suggestions (NEW-1, NEW-22, NEW-23)
…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.
@akshat009
akshat009 merged commit e9bc363 into mainAug 29, 2026
6 checks passed
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.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@akshat009