Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
040df24
docs: point the documentation submodule at block themes and script mo…
ogorzalka Sep 29, 2026
3019593
Merge pull request #363 from Pollora/docs/sync-documentation-submodule
ogorzalka Sep 29, 2026
cc80174
feat: pollora:make:theme activates the theme only where the site need…
ogorzalka Sep 30, 2026
6969508
Merge pull request #364 from Pollora/feature/make-theme-activation
ogorzalka Sep 30, 2026
facdf1a
feat: pollora:doctor and Site Health checks for silent failures
ogorzalka Sep 30, 2026
289fe99
fix: enqueue the Vite client with no version
ogorzalka Sep 30, 2026
d826777
feat: doctor checks cover plugins and modules, dev servers and caches
ogorzalka Sep 30, 2026
b113719
Merge pull request #366 from Pollora/fix/vite-client-version
ogorzalka Sep 30, 2026
24f068e
fix: stop comparing a development build with releases
ogorzalka Sep 30, 2026
c79ce40
fix: dispatch MenuDeleted with the menu delete_nav_menu passes
ogorzalka Sep 30, 2026
9663069
Merge pull request #367 from Pollora/fix/version-check-dev-builds
ogorzalka Sep 30, 2026
45fa12d
Merge branch 'develop' into feature/pollora-doctor
ogorzalka Sep 30, 2026
1cf9b12
Merge remote-tracking branch 'origin/develop' into fix/menu-delete-event
ogorzalka Sep 30, 2026
87a3d9b
Merge pull request #365 from Pollora/feature/pollora-doctor
ogorzalka Sep 30, 2026
8595a48
Merge pull request #368 from Pollora/fix/menu-delete-event
ogorzalka Sep 30, 2026
b6d19f7
chore: release v13.34.0-beta.2
ogorzalka Sep 30, 2026
a11038c
docs: point the documentation submodule at the current release
ogorzalka Sep 30, 2026
7ba64f9
Merge pull request #369 from Pollora/release/v13.34.0-beta.2
ogorzalka Sep 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,21 @@ All notable changes to the Pollora framework will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased](https://github.com/Pollora/framework/compare/v13.34.0-beta...develop)
## [Unreleased](https://github.com/Pollora/framework/compare/v13.34.0-beta.2...develop)

## [v13.34.0-beta.2](https://github.com/Pollora/framework/compare/v13.34.0-beta...v13.34.0-beta.2) - 2026-09-30

### Added
- `pollora:doctor`: checks a project for the failures that stay silent — the site renders, the command exits 0 — and prints, under each, the command that fixes it. Each check comes from a failure met in practice: WordPress core not patched or `__()` not Pollora's; `patches.lock.json` missing or older than the framework's patches; `.env` names Pollora does not read (`DB_NAME`, `WP_HOME`…) or MySQL settings on a sqlite connection; configuration or routes cached outside production; classes missing from the discovery cache; for the theme, every Pollora plugin and every enabled module: a build missing, written to another folder than Pollora reads, or a hot file pointing at a dev server that is stopped or not exposed, a directory symlinked under another name, `%theme_*%`/`%plugin_*%` placeholders or `.stub` files left from a copied template, blocks still in the legacy `resources/blocks`; pattern files WordPress never registers or has not cached; `Route::wp()` routes answering in place of a block theme's templates. `--json` for scripts; exits 1 on an error
- The same checks in WordPress's **Site Health** (Tools › Site Health), with a "Pollora" badge, plus one that only a web request can make: every block of the theme, the Pollora plugins and the modules is registered. That is the boot a visitor gets — blocks were once registered under WP-CLI but not over HTTP, which a console check would have passed

### Fixed
- Deleting a navigation menu (`wp menu delete`, or the Menus screen) raised a `TypeError` once the menu was already gone: `delete_nav_menu` is the `delete_{$taxonomy}` hook, which passes the term ID first, and the listener expected a `WP_Term`. `MenuDeleted` now receives the menu WordPress copied before deleting it
- While Vite ran hot, its client was enqueued with WordPress's version appended (`@vite/client?ver=7.1.2`). The modules Vite serves import `/@vite/client` by its bare URL, so the browser loaded the client twice, as two modules with two HMR connections. It is enqueued with no version, like the entries (regression from v13.34.0-beta)
- On a branch install (`dev-develop`, `13.x-dev`), Site Health and the admin notice announced "Pollora 13.4.4 is available": `version_compare()` ranks a branch name below every release. A development build is no longer compared with releases — Site Health says it is one, the notice stays silent — and the dashboard, the admin menu badge and `pollora:status` share that one rule, which they each duplicated and missed `13.x-dev`. Site Health's info tab now labels the compared version "Latest stable version", since pre-releases are not counted

### Changed
- `pollora:make:theme` activates the generated theme only where the site needs one. A site with no usable theme — a first install — gets it without a question; a site that already has one keeps it unless the answer is "yes", now the default "no", so `--no-interaction` never replaces a working theme. `--activate` and `--no-activate` settle it without asking, and `pollora:install` passes `--activate`. Activation goes through `switch_theme()`, which fires `switch_theme` and `after_switch_theme`; the options were written directly before, so those hooks never ran

## [v13.34.0-beta](https://github.com/Pollora/framework/compare/v13.32.0-beta.9...v13.34.0-beta) - 2026-09-29

Expand Down
2 changes: 1 addition & 1 deletion documentation
4 changes: 3 additions & 1 deletion src/Asset/Infrastructure/Services/AssetEnqueuer.php
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,9 @@ protected function loadViteClient(string $hook): void
$url = $this->viteManager instanceof ViteManager ? $this->viteManager->clientUrl() : '';

if ($url !== '') {
wp_enqueue_script_module('vite-client/'.md5($url), $url);
// No version: the modules Vite serves import /@vite/client by its bare URL,
// and a ?ver= copy would be a second client, with its own HMR connection.
wp_enqueue_script_module('vite-client/'.md5($url), $url, [], null);
}
}, 1);

Expand Down
3 changes: 2 additions & 1 deletion src/Dashboard/Domain/Services/SystemInfoCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,15 @@ private function collectLaravelLocale(): string
}

/**
* @return array{current: ?string, latest: ?string, update_available: bool}
* @return array{current: ?string, latest: ?string, update_available: bool, development: bool}
*/
public function collectFrameworkInfo(): array
{
return [
'current' => $this->versionComparator->getCurrentVersion(),
'latest' => $this->versionComparator->getLatestVersion(),
'update_available' => $this->versionComparator->isUpdateAvailable(),
'development' => $this->versionComparator->isDevelopmentBuild(),
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,7 @@ private function countNotifications(): int
$count = 0;

try {
$comparator = $this->app->make(VersionComparator::class);
$current = $comparator->getCurrentVersion();
$isDev = is_string($current) && str_starts_with($current, 'dev-');

if (! $isDev && $comparator->isUpdateAvailable()) {
if ($this->app->make(VersionComparator::class)->isUpdateAvailable()) {
$count++;
}
} catch (\Throwable) {
Expand Down
4 changes: 2 additions & 2 deletions src/Dashboard/UI/Console/StatusCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,14 @@ private function renderTranslations(array $translations): void
}

/**
* @param array{current: ?string, latest: ?string, update_available: bool} $framework
* @param array{current: ?string, latest: ?string, update_available: bool, development: bool} $framework
*/
private function renderFrameworkStatus(array $framework): void
{
$current = $framework['current'] ?? 'unknown';
$latest = $framework['latest'] ?? 'unknown';
$updateAvailable = $framework['update_available'];
$isDev = is_string($current) && str_starts_with($current, 'dev-');
$isDev = $framework['development'];

if ($isDev) {
$this->line(sprintf('Pollora %s (latest stable: v%s)', $current, $latest));
Expand Down
4 changes: 2 additions & 2 deletions src/Dashboard/UI/Http/DashboardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,13 @@ private function renderStyles(): void
}

/**
* @param array{current: ?string, latest: ?string, update_available: bool} $framework
* @param array{current: ?string, latest: ?string, update_available: bool, development: bool} $framework
*/
private function renderHeader(array $framework): void
{
$current = $framework['current'] ?? __('Unknown', 'pollora');
$updateAvailable = $framework['update_available'];
$isDev = is_string($current) && str_starts_with($current, 'dev-');
$isDev = $framework['development'];

$logoPath = dirname(__DIR__, 4).'/resources/images/pollora-logo.svg';

Expand Down
40 changes: 40 additions & 0 deletions src/Discovery/Infrastructure/Services/DiscoveryCacheManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,46 @@ public function clearCache(iterable $locations): void
}
}

/**
* Classes on disk that the cached entry for a location does not list.
*
* They stay invisible to discovery — no hook, no post type, no route — until
* the cache is cleared, and nothing says so. Null when the location has no
* cached entry, or the cache is off, since then discovery reads the disk.
*
* @return list<string>|null
*/
public function classesMissingFromCache(DiscoveryLocationInterface $location): ?array
{
if (! $this->shouldUseCache()) {
return null;
}

$cacheId = $this->generateCacheId($location);

if (! $this->cacheDriver->has($cacheId)) {
return null;
}

$names = static fn (array $structures): array => array_map(
static fn (mixed $structure): string => is_object($structure) && method_exists($structure, 'getFcqn') ? $structure->getFcqn() : (string) $structure,
$structures,
);

return array_values(array_diff(
$names(Discover::in($location->getPath())->full()->get()),
$names($this->cacheDriver->get($cacheId)),
));
}

/**
* Whether discovery reads through a persistent cache (it does not in debug mode).
*/
public function isCacheEnabled(): bool
{
return $this->shouldUseCache();
}

/**
* Get the cache driver instance.
*/
Expand Down
63 changes: 63 additions & 0 deletions src/Doctor/Application/Services/Doctor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace Pollora\Doctor\Application\Services;

use Pollora\Doctor\Domain\Contracts\CheckInterface;
use Pollora\Doctor\Domain\Enums\RunContext;
use Pollora\Doctor\Domain\Models\CheckResult;
use Throwable;

/**
* Runs the checks that belong to a context.
*
* A check that throws is reported as an error of that check, with the exception's
* message: one broken check must not hide the verdict of all the others.
*/
final readonly class Doctor
{
/**
* @param iterable<CheckInterface> $checks
*/
public function __construct(private iterable $checks) {}

/**
* @return list<CheckInterface>
*/
public function checksFor(RunContext $context): array
{
$checks = [];

foreach ($this->checks as $check) {
if (in_array($context, $check->runsIn(), true)) {
$checks[] = $check;
}
}

return $checks;
}

public function runCheck(CheckInterface $check, RunContext $context): CheckResult
{
try {
return $check->run($context);
} catch (Throwable $throwable) {
return CheckResult::error(
'The check itself failed: '.$throwable->getMessage(),
[$throwable::class.' in '.$throwable->getFile().':'.$throwable->getLine()],
);
}
}

/**
* @return list<array{check: CheckInterface, result: CheckResult}>
*/
public function run(RunContext $context): array
{
return array_map(
fn (CheckInterface $check): array => ['check' => $check, 'result' => $this->runCheck($check, $context)],
$this->checksFor($context),
);
}
}
33 changes: 33 additions & 0 deletions src/Doctor/Domain/Contracts/CheckInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Pollora\Doctor\Domain\Contracts;

use Pollora\Doctor\Domain\Enums\RunContext;
use Pollora\Doctor\Domain\Models\CheckResult;

/**
* One thing `pollora:doctor` and Site Health look at.
*
* A check earns its place by naming a failure that has happened and stays silent
* — the site renders, the command exits 0 — and by saying how to fix it. It
* reads; it never writes, and never requests the site over HTTP.
*/
interface CheckInterface
{
/** Stable identifier, kebab-case: the key of the JSON output and of the Site Health test. */
public function id(): string;

/** What is checked, for a person. */
public function label(): string;

/**
* Where the check can reach a verdict it can stand behind.
*
* @return list<RunContext>
*/
public function runsIn(): array;

public function run(RunContext $context): CheckResult;
}
19 changes: 19 additions & 0 deletions src/Doctor/Domain/Enums/CheckStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Pollora\Doctor\Domain\Enums;

/**
* The verdict of one doctor check.
*
* Skipped is a verdict of its own, never folded into Ok: a check that could not
* run says so, rather than passing a site it never looked at.
*/
enum CheckStatus: string
{
case Ok = 'ok';
case Warning = 'warning';
case Error = 'error';
case Skipped = 'skipped';
}
18 changes: 18 additions & 0 deletions src/Doctor/Domain/Enums/RunContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Pollora\Doctor\Domain\Enums;

/**
* Where a check runs.
*
* Some failures only exist in a web request — blocks registered under WP-CLI but
* not over HTTP, once — so those checks run in WordPress's Site Health, never in
* the console, where they would pass a broken site.
*/
enum RunContext: string
{
case Console = 'console';
case Http = 'http';
}
66 changes: 66 additions & 0 deletions src/Doctor/Domain/Models/CheckResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace Pollora\Doctor\Domain\Models;

use Pollora\Doctor\Domain\Enums\CheckStatus;

/**
* What a check found: a one-line summary, the details that prove it, and, when
* something is wrong, what to do about it.
*/
final readonly class CheckResult
{
/**
* @param list<string> $details
*/
private function __construct(
public CheckStatus $status,
public string $summary,
public array $details = [],
public ?string $fix = null,
) {}

/**
* @param list<string> $details
*/
public static function ok(string $summary, array $details = []): self
{
return new self(CheckStatus::Ok, $summary, $details);
}

/**
* @param list<string> $details
*/
public static function warning(string $summary, array $details = [], ?string $fix = null): self
{
return new self(CheckStatus::Warning, $summary, $details, $fix);
}

/**
* @param list<string> $details
*/
public static function error(string $summary, array $details = [], ?string $fix = null): self
{
return new self(CheckStatus::Error, $summary, $details, $fix);
}

public static function skipped(string $reason): self
{
return new self(CheckStatus::Skipped, $reason);
}

/**
* @return array{status: string, summary: string, details: list<string>, fix: ?string}
*/
public function toArray(): array
{
return [
'status' => $this->status->value,
'summary' => $this->summary,
'details' => $this->details,
'fix' => $this->fix,
];
}
}
Loading
Loading