Skip to content

refactor(theming): move CSS injection from boot() to render events - #164

Merged
rubenvdlinde merged 1 commit into
developmentfrom
wip/render-event-injection
Jul 23, 2026
Merged

refactor(theming): move CSS injection from boot() to render events#164
rubenvdlinde merged 1 commit into
developmentfrom
wip/render-event-injection

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

What / Why

Application::boot() injected all nldesign CSS on every request — including WebDAV, OCS/API, and cron requests that never render a template — via OCP\Util::addStyle(). This change moves injection onto BeforeTemplateRenderedEvent/BeforeLoginTemplateRenderedEvent (the same two events Nextcloud core's own ThemeInjectionService uses), so only actual template renders pay for it.

  • New lib/Service/CssInjectionService.php: the former injectThemeCSS() body moved verbatim (same stylesheet cascade, same order, including the custom-font <link> from the merged custom-font-upload change), now parameterized by render context. Adds occ-only themed_contexts appconfig gating — absent by default, so every surface (login/user/guest/public/error) is themed exactly as before.
  • New lib/Listener/ThemeInjectionListener.php: handles both events, maps renderAs → context (unmapped/future values fail open to themed), applies the per-app exclusion guard (resolved from TemplateResponse::getApp(), falling back to the existing path-regex), and fails open (never breaks rendering) on any internal error.
  • Application::boot() is now a no-op; the dead use OCA\NLDesign\Themes\NLDesignTheme; import is removed.
  • appinfo/info.xml version bumped (cache-buster) + CHANGELOG entry.

See openspec/changes/render-event-injection/ (proposal/design/tasks + spec deltas on css-architecture and per-app-theming) for the full rationale.

Tests

  • 27 new unit tests: tests/Unit/Service/CssInjectionServiceTest.php (14) + tests/Unit/Listener/ThemeInjectionListenerTest.php (13) — cover stylesheet order/gating, all renderAs→context mappings, per-app guard precedence/fallback/fail-open, double-dispatch, and listener failure containment.
  • No pre-existing tests targeted the old private Application::injectThemeCSS()/isThemingDisabled() to retire (verified via grep) — nothing to migrate.
  • Full suite in nextcloud:34.0.0-apache (excluding the one file blocked by the pre-existing, unrelated OC\Mail\EMailTemplate harness gap): 338 tests, 2248 assertions — all green.
  • composer check:strict: lint/phpcs/phpmd/psalm/phpstan all pass. test:all inside check:strict dies at suite-load on the same pre-existing OC\Mail\EMailTemplate gap (reproduces identically on development, unrelated to this change).

Deferred to post-merge (live 8080 verification — see tasks.md §5)

  • Login/user/public-share page curl parity checks (stylesheet link set identical to pre-change)
  • Per-app exclusion regression against a live instance
  • API-surface negative check + log grep for listener errors
  • Browser visual check (login/Files/public share)
  • themed_contexts occ smoke test (set/delete)

Style injection now runs on BeforeTemplateRenderedEvent and
BeforeLoginTemplateRenderedEvent (via a new ThemeInjectionListener) instead
of Application::boot() on every request. This means WebDAV/OCS/API/cron
requests no longer pay for config reads, service resolution, and the
custom-overrides filesystem check when no template is ever rendered.

The injection body itself moves verbatim into a new CssInjectionService,
which also gains render-context discrimination (occ-only `themed_contexts`
appconfig; absent by default, so every surface is themed exactly as before).
The per-app exclusion guard moves into the listener and resolves the app id
from TemplateResponse::getApp() first, falling back to the existing path
regex. Also removes the verified-dead `OCA\NLDesign\Themes\NLDesignTheme`
import from Application.php.

Tests: 27 new unit tests (CssInjectionServiceTest, ThemeInjectionListenerTest).
Full suite (excluding the pre-existing, unrelated OC\Mail\EMailTemplate
harness gap) is green: 338 tests, 2248 assertions. composer check:strict:
lint/phpcs/phpmd/psalm/phpstan all pass.

Live regression verification (login/user/public-share stylesheet parity,
per-app exclusion, context-gating smoke test) is deferred to post-merge —
see openspec/changes/render-event-injection/tasks.md section 5.
@rubenvdlinde
rubenvdlinde merged commit c84eeec into development Jul 23, 2026
25 of 26 checks passed
@rubenvdlinde
rubenvdlinde deleted the wip/render-event-injection branch July 23, 2026 22:28
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/nldesign @ 8652569

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm ✅ 7/7
PHPUnit
Newman ⏭️
Playwright ⏭️

Coverage: 0% (0/5 statements)


Quality workflow — 2026-07-23 22:29 UTC

Download the full PDF report from the workflow artifacts.

rubenvdlinde added a commit that referenced this pull request Aug 5, 2026
…failing CI (#223)

v1.0.1 is `f4d9756` (2026-08-03) and predates three gate fixes, so every
Hydra Gates run this repo has ever made executed a script in which 16
gates reported PASS when their helper never ran (ConductionNL/.github#147),
gate-33 had no axe report to read and never said so (#148), and gates 6
and 7 reported PASS on an empty scope (#149). The tick was identical
either way, which is why nothing in this repo's history shows it.

That pin is now also RED, and the mechanism is worth writing down.
quality.yml is referenced `@main` while this package is PINNED, so the
two can desync. #164 flipped `hydra-gates-require-full-coverage` to
default true in the shared workflow, and that flag requires a gate to
DECLARE itself not-applicable. v1.0.1 contains ZERO `_skip` calls; v1.3.0
has 36. v1.0.1 has no vocabulary to declare, so every absent prerequisite
became "DID NOT RUN" and failed the job — for gates the repo has no
subject matter for.

Measured on this branch, diff-scoped against origin/development exactly
as CI scopes it, in a private mount namespace with a private tmpfs (the
runner's ~50 /tmp/hydra-gate-*.log paths are shared state and two
concurrent runs corrupt each other's counts, .github#158 item 6):

  v1.0.1  exit 98  FAIL — "GATES THAT DID NOT RUN: 24 33"
  v1.3.0  exit 0   PASS — those gates named NOT APPLICABLE, with reasons

Independently confirmed end-to-end: doriath#160 changed this one line and
nothing else, and its Hydra Gates job went failure -> success.

v1.3.0 is `f7eaf2a` = .github@main at the time it was cut.

Refs ConductionNL/.github#159
Sign up for free to 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