Migrate frontend to TypeScript with Deno-based tooling (#209)

* Migrate frontend to TypeScript with Deno-based tooling (#206)

Compiles assets/*.ts via sensiolabs/typescript-bundle (standalone SWC
binary) and adds Deno for formatting, linting, type-checking, and
tests, keeping the project's no-Node/npm approach intact. Wires all
four into CI, the Justfile, and the pre-commit hook.

* fix: build TypeScript assets before running PHPUnit in CI

The tests job ran bin/console sass:build but never typescript:build,
so var/typescript/ didn't exist and any page rendering the importmap
(e.g. backoffice/base.html.twig) errored during tests.

* test: add regression test for backoffice navbar-toggler dead target

Guards against the navbar-toggler button pointing at a collapse
target (#navbarSupportedContent) that isn't rendered for the current
user — the bug hit on the login page before #210 restructured the
nav to always render at least one item (the Releases link) regardless
of auth state.

* fix: stop hand-enumerating TS files for deno check

deno check assets/*.ts assets/controllers/*.ts assets/controllers/bo/*.ts
was duplicated in CI and the Justfile, and silently misses any new
controller subdirectory (fmt/lint/test already recurse assets/ via
deno.json). Add a top-level exclude for assets/vendor/ (respected by
all deno subcommands, unlike the per-task include/exclude blocks) so
deno check assets/ can recurse safely instead.

* fix: GitHubReleasesService date parsing and falsy release name

- Move the release-mapping array_map inside the try block so a
  malformed published_at (or any other parse failure) degrades to the
  same empty-list fallback as an HTTP failure, instead of throwing
  uncaught out of the cache callback.
- Stop treating a release literally named "0" as unnamed — the old
  `?:` fallback is falsy for that string and silently substituted the
  tag name instead.
- Render release dates in UTC explicitly; Twig's date filter otherwise
  silently converts to the app's default timezone (Europe/Amsterdam),
  which could show the wrong calendar day for releases near midnight.

* docs: add scope-creep-as-a-service rule to CLAUDE.md

Per user instruction: when a review turns up a real bug outside the
current task's scope, fix it in the same MR (with a regression test)
rather than just reporting it, unless it needs a human design call.
This commit is contained in:
2026-07-12 22:11:00 +02:00
committed by GitHub
parent 4e98909f11
commit b4a27a7c0d
44 changed files with 1473 additions and 509 deletions
+17 -10
View File
@@ -6,6 +6,7 @@ namespace Tvdt\Service;
use Psr\Cache\InvalidArgumentException;
use Safe\DateTimeImmutable;
use Safe\Exceptions\SafeExceptionInterface;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
@@ -44,7 +45,21 @@ final readonly class GitHubReleasesService
/** @var list<array{tag_name: string, name: ?string, published_at: ?string, body: ?string, html_url: string}> $releases */
$releases = $response->toArray();
} catch (ExceptionInterface) {
usort($releases, static fn (array $a, array $b): int => ($b['published_at'] ?? '') <=> ($a['published_at'] ?? ''));
$result = array_map(static function (array $release): array {
$name = $release['name'] ?? '';
return [
'tagName' => $release['tag_name'],
'name' => '' !== $name ? $name : $release['tag_name'],
'publishedAt' => $release['published_at'] ? new DateTimeImmutable($release['published_at']) : null,
'body' => (string) $release['body'],
'url' => $release['html_url'],
];
}, $releases);
} catch (ExceptionInterface|SafeExceptionInterface|\DateMalformedStringException) {
$save = false;
return [];
@@ -52,14 +67,6 @@ final readonly class GitHubReleasesService
$item->expiresAfter(3600);
usort($releases, static fn (array $a, array $b): int => ($b['published_at'] ?? '') <=> ($a['published_at'] ?? ''));
return array_map(static fn (array $release): array => [
'tagName' => $release['tag_name'],
'name' => $release['name'] ?: $release['tag_name'],
'publishedAt' => $release['published_at'] ? new DateTimeImmutable($release['published_at']) : null,
'body' => (string) $release['body'],
'url' => $release['html_url'],
], $releases);
return $result;
}
}