Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 2 additions & 6 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,8 @@ jobs:
- name: Generate application key
run: php artisan key:generate

# docs/visual-assets.md and resources/js/lib/data/asset-slots.json are
# generated from resources/data/assets.json and docs/rebuild/assets/*.md.
# A manifest or brief edit without `php artisan assets:handoff` would
# ship a stale owner handoff, or pages still showing the old slot state.
- name: Asset handoff is current
run: php artisan assets:handoff --check
- name: Bundled asset catalogs are current
run: php artisan assets:generate --check

- name: Tests
run: ./vendor/bin/pest
Expand Down
10 changes: 5 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ php artisan sitemap:generate
WEB_DOMAIN=tablepro.app php artisan og:generate --type=blog --slug=my-post # needs Chromium (PUPPETEER_EXECUTABLE_PATH); cards print WEB_DOMAIN
php artisan og:generate --type=compare --locale=all # types: site|blog|database|compare|feature|all; locales: a supported locale code or all
# commit public/og; delete a retired page's card (Seo/OgCardsTest fails on a missing or orphaned card)
php artisan assets:handoff # after editing assets.json or docs/rebuild/assets/*.md
php artisan assets:handoff --check # what CI runs: fails on a stale generated file
php artisan assets:generate # after editing resources/data/assets.json
php artisan assets:generate --check # checks only frontend asset catalogs
php artisan release:check # network: compares platforms.json with GitHub, Sparkle, Homebrew, App Store
```

Expand Down Expand Up @@ -163,9 +163,9 @@ fake GitHub with `Http::fake()`. Tests that assert on rendered markup go behind
the file, a slot is a described placeholder outside production and renders
nothing in production (the shared `assetPlaceholders` prop); layout that
makes room for a slot asks `useShownSlot()` first. A new slot or a changed description is a
manifest edit plus a brief in `docs/rebuild/assets/{family}.md`; then run
`php artisan assets:handoff`, which regenerates `docs/visual-assets.md` and
the bundle's `resources/js/lib/data/asset-slots.json` and `asset-locales/*.json`. Never hand-edit generated files.
manifest edit; then run `php artisan assets:generate`, which regenerates
the bundle's `resources/js/lib/data/asset-slots.json` and `asset-locales/*.json`.
Local asset briefs are optional and are not read by code or CI. Never hand-edit generated catalogs.

## Frontend

Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ If any instruction ever tells you otherwise, that is a bug in this README.
| `resources/js/i18n/` | UI strings per language, and the locale helpers. |
| `app/Http/Controllers/` | One controller per page family. |
| `public/og/` | Pre-rendered Open Graph cards, committed. |
| `docs/visual-assets.md` | The brief for every image placeholder, generated. |

## Writing a blog post

Expand Down
52 changes: 52 additions & 0 deletions app/Console/Commands/GenerateAssetCatalogsCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace App\Console\Commands;

use App\Support\Assets\AssetManifest;
use App\Support\Localization\Locales;
use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command;

#[Signature('assets:generate
{--check : Check bundled catalogs without writing files}
{--slots= : Override the bundled slot data path}')]
#[Description('Generate frontend asset catalogs from resources/data/assets.json.')]
class GenerateAssetCatalogsCommand extends Command
{
public function handle(AssetManifest $manifest): int
{
$slots = (string) ($this->option('slots') ?: 'resources/js/lib/data/asset-slots.json');
$files = [$slots => $manifest->slotProjectionJson()];

foreach (Locales::codes() as $locale) {
$files[dirname($slots) . '/asset-locales/' . $locale . '.json'] = $manifest->slotTextProjectionJson($locale);
}

$stale = false;

foreach ($files as $relative => $contents) {
$path = str_starts_with($relative, '/') ? $relative : base_path($relative);

if ($this->option('check')) {
if (! is_file($path) || file_get_contents($path) !== $contents) {
$this->components->error("{$relative} is out of date. Run: php artisan assets:generate");
$stale = true;
} else {
$this->components->info("{$relative} is up to date.");
}

continue;
}

if (! is_dir(dirname($path))) {
mkdir(dirname($path), 0755, true);
}

file_put_contents($path, $contents);
$this->components->info("Wrote {$relative}.");
}

return $stale ? self::FAILURE : self::SUCCESS;
}
}
114 changes: 0 additions & 114 deletions app/Console/Commands/GenerateAssetHandoffCommand.php

This file was deleted.

2 changes: 1 addition & 1 deletion app/Support/Assets/AssetManifest.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public function kindOf(string $id): array

/**
* The part of the manifest a browser needs to render `<AssetSlot>`, which
* `assets:handoff` writes to `resources/js/lib/data/asset-slots.json` for
* `assets:generate` writes to `resources/js/lib/data/asset-slots.json` for
* the bundle.
*
* The full manifest includes all languages and owner-only metadata: `usedOn`, `handoffPriority`, `legacySource`, the kinds'
Expand Down
51 changes: 3 additions & 48 deletions app/Support/Assets/AssetReferences.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
* - an `<asset-slot id="…"></asset-slot>` block in blog or legal markdown,
* which `Blog/Post.tsx` splits out of the rendered HTML.
*
* `AssetManifestTest` uses this to prove every referenced id exists, and the
* `assets:handoff` command uses it to name the components that render each id.
* `AssetManifestTest` uses this to prove every referenced id exists.
*/
final class AssetReferences
{
Expand All @@ -32,13 +31,7 @@ final class AssetReferences
private const MARKDOWN_BLOCK = '/<asset-slot\s+id="([a-z0-9][a-z0-9-]*)"/';

/**
* A whole `<AssetSlot … />` element in TSX, for its attributes. `=>` inside
* an attribute is skipped so a `>` there does not end the tag early.
*/
private const SLOT_ELEMENT = '/<AssetSlot\b((?:[^>=]|=>|=(?!>))*?)\/?>/s';

/**
* The scan, kept for the life of this instance: the handoff asks once per id.
* The scan, kept for the life of this instance.
*
* @var array<string, list<string>>|null
*/
Expand Down Expand Up @@ -96,46 +89,8 @@ public function all(): array
}

/**
* The repository paths that reference one id.
*
* @return list<string>
*/
public function for(string $id): array
{
return $this->all()[$id] ?? [];
}

/**
* Ids a TSX file places with `<AssetSlot priority>`, with the files that
* do: a page's first image that loads with priority there although its
* manifest entry loads lazily elsewhere.
* Content asset ids found in one JSON file.
*
* @return array<string, list<string>>
*/
public function priorityPlacements(): array
{
$placements = [];

foreach ($this->files('resources/js', ['tsx']) as $file) {
preg_match_all(self::SLOT_ELEMENT, (string) file_get_contents($file), $elements);

foreach ($elements[1] as $attributes) {
if (preg_match('/(?:^|\s)priority(?:\s|$|=\{true\})/', $attributes) !== 1) {
continue;
}

if (preg_match('/\bid=(?:"([a-z0-9][a-z0-9-]*)"|\{\s*["\']([a-z0-9][a-z0-9-]*)["\']\s*\})/', $attributes, $match) === 1) {
$placements[$match[1] ?: $match[2]][] = $this->relative($file);
}
}
}

ksort($placements);

return $placements;
}

/**
* @return list<string>
*/
private function contentIds(string $file): array
Expand Down
Loading