From 3517185e123ca7ba6eae49140193c6e69c38bd7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Nykl=C3=AD=C4=8Dek?= <60318239+ONyklicek@users.noreply.github.com> Date: Mon, 10 Aug 2026 02:43:51 +0200 Subject: [PATCH] Render every package's assets from one line, and stop dropping the tag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three things in the asset surface, none of which changes what a package already renders. `@packageAssets`, `@packageStyles` and `@packageScripts` now take the short name optionally. Naming none renders every package that declared entries, which is the form an application's own layout wants: a layout that names its packages has to be edited every time one is installed or removed, in every file carrying the line, and `package:discover` does not close that gap because it discovers providers while the template still names packages by hand. Stylesheets lead across the whole set rather than within each package, though not across the seam between the Vite block and the shipped tags — that block is emitted whole so its preloads stay a single set. `hasAssetFallback()` says where to serve a shipped file from when nothing is published. An entry that resolved to nothing rendered no tag at all, which is right for an entry the application declined to build and wrong for the one that is the package's only copy: where `public/` cannot be written, a page lost its stylesheet or its behaviour with nothing in the markup, the log or the console to say why, on exactly the deployments least likely to go looking. A package that also serves its assets from a route of its own now keeps the tag, with `type="module"` or the `defer` that `classic()` implies, its attributes, `data-navigate-track` and the CSP nonce still on it. The resolver is reached only after both the mirror and `public/vendor/{name}` came back empty, so a normal deployment never calls it. `resolution()` no longer reports `shipped` for a mirrored package whose copy can never be written. The mirrored arm asserted it outright, ahead of every arm that checks anything, so an unwritable `public/` looked healthy in `php artisan about` while the page rendered nothing — and it left `fallback` unreachable for any mirrored package, which is to say for the default. A copy already published now earns `shipped` first; a mirrored entry without one earns it only where the mirror could still create it, asked of the nearest existing ancestor of `public/vendor/{name}` so a read-only `public/vendor` under a writable `public/` is not taken for a writable one. The lazy mirror is unaffected: a fresh install, where `about` runs before any request has published anything, still reports `shipped`, and nothing is published to find out. --- CHANGELOG.md | 54 ++++++ README.md | 46 ++++- ai/AGENTS.md | 4 +- docs/api-reference.md | 13 +- docs/assets.md | 88 +++++++++- docs/packager.md | 7 +- docs/service-provider.md | 3 +- docs/upgrade.md | 24 +++ site/templates/home.mjs | 5 + src/Concerns/HasAssets.php | 56 ++++++ .../Concerns/DeclaresPackageAssets.php | 1 + src/Support/PackageAssets.php | 164 +++++++++++++++--- .../PackageAssetAggregateTest.php | 117 +++++++++++++ .../PackageAssetFallbackMirroredTest.php | 129 ++++++++++++++ .../PackageAssetFallbackTest.php | 104 +++++++++++ tests/Unit/HasAssetsTest.php | 21 +++ 16 files changed, 801 insertions(+), 35 deletions(-) create mode 100644 tests/PackageProviderTests/PackageAssetAggregateTest.php create mode 100644 tests/PackageProviderTests/PackageAssetFallbackMirroredTest.php create mode 100644 tests/PackageProviderTests/PackageAssetFallbackTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index dc67b5b..640099b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,60 @@ All notable changes to `laravel-package-toolkit` will be documented in this file The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/). +## [2.4.2] - 2026-08-10 + +### Added + +- **The three tag directives take no package name.** `@packageAssets`, `@packageStyles` and + `@packageScripts` now render every package that declared entries, in the order their providers + handed them over, and the short name is optional rather than required. A layout that names its + packages is a layout that has to be edited every time one is installed or removed, in every file + carrying the line — and `package:discover` does not close that gap, because it discovers + *providers* while the template still names packages by hand. The aggregate is the form an + application's layout wants: one line that keeps saying everything. + Stylesheets lead across the whole set rather than within each package, since the aggregate renders + one document's `` and a package whose provider booted third is no reason for its stylesheet + to land behind the second package's scripts — within each of the two halves, that is, since the + Vite keys are still collected into a single call so the preloads remain one set, and that block is + emitted whole and first. Everything else stays per entry — each package's `classic()`, its + attributes, its own Vite resolution. `@packageAssetUrl` keeps both arguments; it answers + with one URL, and there is no URL of every package. + +- **`hasAssetFallback()` — where to serve a shipped file from when nothing is published.** An entry + that resolved to nothing rendered no tag at all. That is right for an entry the application chose + not to build, and wrong for the entry that is the package's only copy: where `public/` cannot be + written — a read-only container, Vapor, shared hosting — the page lost its stylesheet or its + behaviour with nothing in the markup, the log or the console to say why, on exactly the + deployments least likely to go looking. The documented answer was to call `PublishedAssets::url()` + and compose a tag by hand, which is the pre-2.4.0 pattern the renderer exists to remove. + A package that also serves its assets from a route of its own now points at it and keeps the tag, + with `type="module"` or the `defer` that `classic()` implies, its declared attributes, + `data-navigate-track` and the application's CSP nonce still on it. The resolver + (`fn (string $file, string $package): ?string`) is reached only after both the mirror and + `public/vendor/{short-name}` came back empty, so a normal deployment never calls it, and it owns + the whole URL it returns, cache-busting query string included — the `?id=` the renderer appends + elsewhere is the published copy's mtime, and the point of being there is that there is none. + Returning `null` drops the tag as before, and `resolution()` gained a `fallback` state so a + deployment serving from the route is distinguishable from one serving nothing. + +Both are additive: `@packageAssets('blog')` renders byte for byte what it did, `PackageAssets::declare()` +took a new parameter with a default, and a package that declares no fallback behaves exactly as before. + +### Fixed + +- **`resolution()` no longer reports `shipped` for a mirrored package whose copy can never be + written.** The mirrored arm asserted it outright, ahead of every arm that checks anything, so an + unwritable `public/` — a read-only container, Vapor, shared hosting — reported every entry as + served from `public/` while the page rendered nothing at all, or, once `hasAssetFallback()` + existed, rendered the fallback. That left `fallback` unreachable for any mirrored package, which + is to say for the default, and it left the report wrong in precisely the production condition it + was added to expose. A copy already published now earns `shipped` first; a mirrored entry without + one earns it only where the mirror could still create it, asked of the nearest existing ancestor + of `public/vendor/{short-name}` so a read-only `public/vendor` under a writable `public/` is not + taken for a writable one. The lazy mirror is unaffected — a fresh install, where `about` runs + before any request has published anything, still reports `shipped` — and nothing is published to + find out, as before. + ## [2.4.1] - 2026-08-10 ### Added diff --git a/README.md b/README.md index 105509c..fce6824 100644 --- a/README.md +++ b/README.md @@ -905,6 +905,49 @@ are checked at registration, so a typo throws where it was declared. `.js` rende `type="module"`; a shipped IIFE bundle says so with `Asset::make('js/index.js')->classic()`. +#### Naming no package renders every one + +Since 2.4.2 the short name is optional on the three tag directives — the form an application's own +layout wants, since a layout that names its packages is one that has to be edited every time a +package is installed or removed: + +```blade + + @packageStyles + + + @packageScripts + +``` + +Every package that declared entries renders, in the order their providers handed them over. +Stylesheets lead across the whole set rather than within each package — within each of the two +halves, that is: what the application's Vite build covers is emitted as one block so its preloads +stay a single set, and that block leads. Everything else stays per entry: each package's +`classic()`, its attributes, its own Vite resolution. `@packageAssetUrl` still takes both +arguments — it answers with one URL, and there is no URL of every package. + +#### Keeping the tag when nothing is published + +An entry that resolves to nothing renders no tag. That is right for an entry the application chose +not to build, and wrong for the entry that is your package's only copy: where `public/` cannot be +written — a read-only container, Vapor, shared hosting — the page loses its stylesheet or its +behaviour with nothing to say why. If your package also serves its assets from a route of its own, +say so and the tag survives: + +```php +$packager + ->hasAssets(entries: ['js/index.js']) + ->hasAssetFallback(fn (string $file): string => route('my-package.asset', ['file' => $file])); +``` + +The resolver is reached only after both the mirror and `public/vendor/my-package` came back empty, +so a normal deployment never calls it, and it owns the whole URL including any cache-busting query +string — the `?id=` elsewhere is the published copy's mtime, and the point of being here is that +there is none. Returning `null` drops the tag as before. What declaring it buys is the tag itself: +`type="module"` or the `defer` that `classic()` implies, your attributes, +`data-navigate-track="reload"` and the application's CSP nonce. + #### Naming nothing discovers them Name no entries and the asset directory answers for itself, the way `hasRoutes()` and `hasViews()` @@ -958,7 +1001,8 @@ fine, which is what keeps it in codebases — here is what it leaves out, almost - **`url()` returns `?string`.** Where `public/` cannot be written and nothing was published before, this renders `src=""` — which a browser resolves against the current page and fetches the HTML as a script. Nothing throws, nothing 404s, and the page is broken. The directives emit no tag at all - in that situation. + in that situation, or the one + [`hasAssetFallback()`](#keeping-the-tag-when-nothing-is-published) points at. - **No `type="module"`**, so a Vite bundle's top-level `import` is a syntax error. - **No `data-navigate-track="reload"`**, which makes `?id=` a query string nobody reads: Livewire has no reason to full-page-reload a `wire:navigate` visit, so an upgrade lands as new diff --git a/ai/AGENTS.md b/ai/AGENTS.md index 0089f1c..224d9ae 100644 --- a/ai/AGENTS.md +++ b/ai/AGENTS.md @@ -159,9 +159,11 @@ $packager->hasOptimizeCommands( - **Do not build a tag around it in a template.** `