Skip to content

Add scripts and styles as a capability the plugin orchestrates - #177

Merged
schlessera merged 4 commits into
mainfrom
feat/37-asset-capability
Oct 24, 2026
Merged

schlessera merged 4 commits into
mainfrom
feat/37-asset-capability

Conversation

@schlessera

@schlessera schlessera commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Refs #37

What changed

A service can now declare the scripts and styles it needs by implementing HasAssets. It returns immutable Style and Script declarations from get_assets( AssetContext ), and the plugin orchestrates the rest. If any declared service has assets, the plugin hooks wp_enqueue_scripts, admin_enqueue_scripts (keeping $hook_suffix), enqueue_block_editor_assets and login_enqueue_scripts, once each. At each of those actions it asks every registered HasAssets service and hands the result to an Assets loader (WordPressAssets by default; it is a trailing optional constructor parameter).

How the pieces behave:

  • Where files are served from. Asset URLs and versions come from get_asset_location(). By default that is the main file's directory URL plus its Version header. No constant is read, and a missing header is an InvalidAsset.
  • Late registration. A service registered after an enqueue action has run is asked for it at once, the way Delayed handles an action that has passed.
  • Repeated handles. An identical declaration does nothing; a conflicting one throws in debug mode and is skipped in production.
  • Failures. They are rethrown unchanged in debug mode. In production they skip that service's remaining assets for that enqueue point, and the other services still load theirs.

The two samples now use it:

  • SampleLoopService declares a stylesheet for its post header on the front end. The service only exists on singular views.
  • SampleBackendService declares a deferred "Copy the plugin name" script for admin screens only.

The architecture rules learn the new capability:

  • prefixedNames checks the handle where it is declared (Style::file(), Style::inline(), Script::file(), including dynamic receivers). WordPressAssets is listed as a forwarder.
  • capabilitiesOnServices accepts Delayed + HasAssets as having work to run.
  • dependencyDirection lets a subsystem name the infrastructure's value objects, but not a top-level class.

Why this shape

The design is the ruled proposal on #37, built on navnorth/museable's Asset/Style/Script, minus its defects: global constants, build-path rewriting, hardcoded versions, per-service enqueue hooks, and render-time enqueueing, which leaves content unstyled until the footer.

Two places deviate from the proposal text, both because review found real bugs:

  • The asset location and the debug mode for the asset boundary are protected template methods (get_asset_location(), get_debug_mode()), not default injector delegations. A default delegation silently replaced a DebugMode delegation configured on an injector passed into the constructor, for every plugin, with or without assets. Template methods sit next to get_main_file() and leave the injector alone.
  • A repeated declaration is compared by what WordPress makes of it, not by raw registry fields. The legacy footer argument 1, an empty strategy, and the wp-i18n dependency that wp_set_script_translations() adds all count as equal.

A second, independent review ran on the branch. Its findings are fixed in the third commit:

  • re-entrant dispatch;
  • %2e%2e and control characters in paths;
  • ? and whitespace in handles;
  • dynamic-class calls bypassing the prefix rule;
  • a forwarder's own literal names going unchecked;
  • the value-object allowance leaking to top-level classes;
  • the sample script failing before WordPress 6.3;
  • the error message advising an impossible fix;
  • weak immutability, Error and priority-order tests.

Proof

composer test   # 711 tests, 3852 assertions, PHP 8.3, rebased on main at 1228940
composer lint   # phpcs, PHPStan max, Psalm 2, rule tests: clean
  • PHP 7.4.33, with its own resolved dependency set as CI does: 670 tests pass (5 skipped as 8.0+ only).
  • Mutation checks: removing the late catch-up, or the re-entrancy guard, each fails exactly the test written for it.

Checklist

  • composer test and composer lint pass locally, and CI is green on the current head.
  • Every acceptance criterion from the issue is met, or the gap is named below.
  • Nothing a generated plugin relies on changed, or the issue carries breaking and a ruling.
  • Docs and inline comments reflect the change, or nothing documented moved.

Anything left open

@schlessera schlessera added type: feat A new capability or a change in behaviour. area: infrastructure src/Infrastructure: services, injector, views, autoloader. What generated plugins build on. breaking Changes an interface or convention generated plugins rely on. Needs a ruling before code. labels Oct 24, 2026
Keep a supplied injector's configuration: the asset location and the
debug mode the asset loading checks are template methods now, not
injector defaults. Guard against an enqueue action fired inside one,
validate handles and paths against what WordPress and browsers do
with them, compare script registrations by what they mean, check
dynamic declaration calls and a forwarder's own names, keep the
value-object allowance to subsystems, and load the sample script
once the notice is there on WordPress before 6.3.
The asset declarations moved the sample services' methods down, so
the citations of them in the patterns and principles are updated to
where the same code is now.
@schlessera
schlessera force-pushed the feat/37-asset-capability branch from c72bf64 to be421b6 Compare October 24, 2026 00:23
schlessera added a commit to mwpd/content-freshness that referenced this pull request Oct 24, 2026
Follows mwpd/basic-scaffold@c72bf64 (mwpd/basic-scaffold#177): the plugin no
longer delegates AssetLocation and DebugMode in the injector but asks the
template methods get_asset_location() and get_debug_mode(), an enqueue action
fired from inside get_assets() is only remembered, handles are AssetHandle
values that reject what WordPress cannot print, equivalent script
registrations compare equal, and the prefix rule sees dynamic receivers.

The composition root overrides get_debug_mode() to return the DebugMode its
services are bound to, so one rebinding covers the notice and the plugin's
asset boundary alike.

Refs #2
@schlessera
schlessera merged commit 15cac4c into main Oct 24, 2026
5 checks passed
schlessera added a commit to mwpd/content-freshness that referenced this pull request Oct 24, 2026
The notice now has a default style, loaded in the head only on outdated singular posts through the asset capability ported from mwpd/basic-scaffold#177. Refs #2.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: infrastructure src/Infrastructure: services, injector, views, autoloader. What generated plugins build on. breaking Changes an interface or convention generated plugins rely on. Needs a ruling before code. type: feat A new capability or a change in behaviour.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant