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.** `