Files
Obsidian-Vault/Work/Projects/Laravel-v12-Migration/Logs/upgrade-log-contentmakers.md
Vincent Verbruggen 1999dbbc11 Add upgrade logs for Laravel 11 to 12 migration across multiple projects
- Created detailed upgrade log for deversspecialist with phases including Composer updates, Filament upgrades, Tailwind migration, Pest updates, and final verification.
- Added upgrade log for goos project, documenting version changes, encountered issues, and resolutions during the migration process.
- Documented Composer dependency upgrades for qlshandling, highlighting significant changes and necessary code adjustments.
- Introduced a template for future upgrade logs to standardize documentation for Laravel migrations.
2026-02-20 10:43:24 +01:00

30 KiB

Update Log

Run Metadata

  • Date: 2026-02-10
  • Started: 2026-02-10 12:36:36 CET
  • Branch: upgrade/laravel12-filament5-full
  • Goal: Laravel 12 + full ecosystem upgrades (Composer and npm), including Filament v3 -> v4 -> v5, while skipping ESLint major upgrades.

Preflight Snapshot

Environment

  • PHP: 8.4.17
  • Composer: 2.8.5
  • Node: v20.19.6
  • npm: 11.6.4

Composer Outdated (Direct)

  • Laravel major target available: laravel/framework 11.46.1 -> 12.50.0.
  • Filament major target available: filament/filament 3.3.45 -> 5.2.0.
  • Inertia Laravel major target available: 1.3.3 -> 2.0.19.
  • Ziggy major target available: 1.8.2 -> 2.6.0.
  • Multiple ecosystem majors available (Filament plugins, DOMPDF, Mollie, Pest stack, Symfony components, Resend).

Laravel 12 Blocker Check (composer why-not laravel/framework ^12 -t)

  • Hard blockers detected:
    • codezero/laravel-localized-routes:^4 (supports only Illuminate 10/11).
    • codezero/laravel-localizer:^3 (supports only Illuminate 10/11).
    • codezero/laravel-uri-translator:^2 (transitive, supports only Illuminate 10/11).
    • resend/resend-laravel:^0.14 (supports only Illuminate 10/11).
    • laravel-lang current line (lang:^12, common:^3) anchored to publisher line that supports only Illuminate < =11.

npm Outdated Snapshot

  • Major targets available:
    • @inertiajs/vue3 1.3.0 -> 2.3.13
    • tailwindcss 3.4.18 -> 4.1.18
    • laravel-vite-plugin 1.3.0 -> 2.1.0
    • vite 6.4.1 -> 7.3.1
    • @vitejs/plugin-vue 5.2.4 -> 6.0.4
    • pinia 2.3.1 -> 3.0.4
    • swiper 10.3.1 -> 12.1.0
    • vue-i18n 10.0.8 -> 11.2.8
  • ESLint majors are available but intentionally skipped in this run.

Baseline Verification

  • php artisan test --compact: 14 passed, no failures.
  • Warnings: PHPUnit doc-comment metadata deprecations (will break in PHPUnit 12 timeframe).
  • npm run build: build succeeds.
  • Warning: baseline-browser-mapping database is older than two months.

Package Blockers

  • codezero/laravel-localizer removed per decision (no Laravel 12 support).
  • codezero/laravel-localized-routes replaced by opgginc/codezero-laravel-localized-routes (Laravel 12 support).
  • resend/resend-laravel must move to ^1.0.
  • laravel-lang/lang and laravel-lang/common must move to newer major lines compatible with Laravel 12.

Exceptions & Weirdness

  1. Stale package/service manifests after Composer package changes

    • Symptom: runtime errors for removed/moved service providers (for example debugbar, old localizer, filament-upgrade provider references).
    • Failing commands: php artisan ... commands after composer update --no-scripts.
    • Root cause: stale bootstrap/cache/packages.php and bootstrap/cache/services.php.
    • Exact fix: delete both files and re-run composer dump-autoload / package discovery.
    • Preventive rule: clear both manifest files after major package removals/replacements.
  2. Localized route transition edge case (Attribute [localized] does not exist)

    • Symptom: route boot failure during transition from abandoned CodeZero packages to OPGG fork.
    • Root cause: cached package manifests + package transition timing.
    • Exact fix: same manifest clear as above, then rerun discovery.
  3. Ziggy v2 client dependency gap

    • Symptom: Vite build failed with Rollup failed to resolve import "qs-esm" from vendor/tightenco/ziggy/src/js/Route.js.
    • Failing command: npm run build.
    • Root cause: Ziggy v2 JS source now imports qs-esm, not present in project npm deps.
    • Exact fix: npm install qs-esm.
    • Preventive rule: after Ziggy major upgrades, run a full build immediately and verify vendor JS imports.
  4. PHPUnit 12 test discovery change reduced executed test count (14 -> 3)

    • Symptom: only methods named test_* executed; legacy /** @test */ methods were skipped.
    • Failing command: php artisan test --compact (unexpected low count).
    • Root cause: metadata annotation behavior changed in PHPUnit 12 line.
    • Exact fix: migrate /** @test */ methods to #[Test] attributes in affected class-based tests.
    • Preventive rule: include a test-count sanity check after PHPUnit major upgrades.
  5. Hidden regression surfaced after restoring full test discovery

    • Symptom: Call to undefined function get_classes_in_namespace() in sitemap generation test.
    • Failing command: php artisan test --compact.
    • Root cause: helper function no longer provided by dependencies after package upgrades.
    • Exact fix: added local get_classes_in_namespace() implementation to app/helpers.php.
    • Preventive rule: once discovery is fixed, rerun full suite and treat newly exposed failures as real upgrade regressions.
  6. Pint interaction with PHPUnit setup visibility

    • Symptom: setUp() visibility conflict (must be public) after formatting pass.
    • Failing command: php artisan test --compact.
    • Root cause: formatter rule normalized setup method visibility in a way that conflicted with this TestCase chain.
    • Exact fix: remove no-op setUp() overrides from affected tests.
    • Preventive rule: rerun tests after Pint in upgrade branches, not just before Pint.
  7. npm package location warning due duplicate entries

    • Symptom: npm warned about removing duplicated packages from dependencies in favor of devDependencies.
    • Root cause: same frontend build packages declared in both sections.
    • Exact fix: npm normalized entries during install (kept in devDependencies).
    • Preventive rule: avoid duplicating build-only packages across both dependency sections.
  8. Build warning persisted (non-blocking)

    • Symptom: [baseline-browser-mapping] ... over two months old and generated empty chunks in Vite output.
    • Impact: build still successful.
    • Action: log-only.
  9. Filament v5 codemod scope gap caused delayed runtime failures

    • Symptom: project appeared upgraded, but frontend routes later crashed with missing Filament classes in cms/* and commerce/*.
    • Root cause: initial codemod pass was executed for app/ and modules/ only, leaving other custom source roots untouched.
    • Exact fix: run a second migration pass over all custom roots and then verify class existence across repository imports.
    • Preventive rule: always enumerate all custom source roots before running Filament codemods (app, modules, cms, commerce, support, etc.).
  10. vendor/bin/filament-v5 can print Rector cache deletion errors but still exit successfully

  • Symptom: commands like vendor/bin/filament-v5 cms and vendor/bin/filament-v5 commerce reported Unable to delete ... rector_cached_files ... No such file or directory, yet finished with success messaging.
  • Impact: misleading signal that migration completed for target directory.
  • Exact fix: do not trust command success text alone; run a post-codemod class-import existence scan and targeted runtime smoke checks.
  • Preventive rule: treat codemod output warnings as potential partial-failure even with zero exit code.
  1. Filament v5 namespace migration gaps (schemas vs forms)
  • Symptom: Class "Filament\\Forms\\Components\\Fieldset" not found on runtime page render.
  • Root cause: layout/util classes moved to Filament\\Schemas\\... but legacy imports remained in cms/Filament/Form/*.
  • Exact fix: migrate imports:
    • Filament\\Forms\\Components\\Fieldset -> Filament\\Schemas\\Components\\Fieldset
    • Filament\\Forms\\Components\\Grid -> Filament\\Schemas\\Components\\Grid
    • Filament\\Forms\\Components\\Component -> Filament\\Schemas\\Components\\Component
    • Filament\\Forms\\Get|Set -> Filament\\Schemas\\Components\\Utilities\\Get|Set
    • Filament\\Forms\\Components\\Tabs\\Tab -> Filament\\Schemas\\Components\\Tabs\\Tab
    • Filament\\Forms\\Components\\Section -> Filament\\Schemas\\Components\\Section
  • Preventive rule: after each major Filament upgrade, scan for old Filament\\Forms\\Components\\(Fieldset|Grid|Section|Tabs\\Tab) and Filament\\Forms\\Get|Set.
  1. Filament page actions namespace changed
  • Symptom: commerce resource page classes still imported Filament\\Pages\\Actions.
  • Root cause: missed codemod transformations in specific folders.
  • Exact fix: replace with use Filament\\Actions; and keep Actions\\*Action::make() calls.
  • Preventive rule: grep for use Filament\\Pages\\Actions; after v5 migration and replace all matches.
  1. Resource/relation manager form signature drift
  • Symptom: files still used Filament\\Forms\\Form type and form(Form $form): Form signatures.
  • Root cause: partial codemod application in commerce resources.
  • Exact fix: update to Filament v5 schema signature:
    • Resources: public static function form(Schema $schema): Schema
    • Relation managers: public function form(Schema $schema): Schema
  • Preventive rule: grep for use Filament\\Forms\\Form; and remove all remaining occurrences.
  1. TextInput\\Mask builder class no longer available in current Filament line
  • Symptom: legacy closures typed as Forms\\Components\\TextInput\\Mask became invalid and future runtime failures were likely.
  • Root cause: legacy mask builder API from older Filament line.
  • Exact fix: replaced with v5-safe numeric/step/prefix configuration instead of removed mask-builder typehints.
  • Preventive rule: grep for TextInput\\Mask and rewrite masks using current v5 text input API.
  1. Running the official Filament upgrade utility can generate a large public-asset diff
  • Symptom: php artisan filament:upgrade --no-interaction republished many files under public/js/filament/*, public/css/filament/*, and public/fonts/filament/*, then cleared config/route/view caches.
  • Impact: noisy git diff and potential overwrite risk for locally customized published Filament assets.
  • Exact fix: run this command intentionally once per upgrade pass, then review/commit asset churn separately and rerun verification (pint, tests, build).
  • Preventive rule: announce this side effect before execution and avoid running it repeatedly without need.
  1. route:list can throw due controller-constructor abort guards
  • Symptom: php artisan route:list --path=admin failed with Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException originating from App\\Http\\Controllers\\Public\\VacancyController::__construct() line 16.
  • Root cause: constructor contains abort_unless(..., 404) and the route-list command instantiates controllers during route metadata collection.
  • Exact fix: use php artisan about + tests/build for upgrade smoke checks when this occurs. Long-term refactor: move hard feature gates out of constructors into middleware/action-level guards.
  • Preventive rule: avoid hard abort_* calls in controller constructors.
  1. npm dependency graph got stuck during ESLint major migration
  • Symptom: repeated npm install attempts failed with ERESOLVE could not resolve while moving eslint-plugin-vue to ^10 and vue-eslint-parser to ^10.
  • Root cause: stale lock/tree state from earlier duplicated dependency declarations caused resolver deadlock.
  • Exact fix: regenerate the npm install tree cleanly (fresh node_modules and lock regeneration), then install the target dependency set.
  • Preventive rule: when npm repeatedly fails with the same peer conflict after package.json corrections, do a clean dependency graph rebuild instead of retry loops.
  1. eslint-plugin-vue v10 required ESLint flat config migration
  • Symptom: linting failed with legacy config errors (plugin:vue/vue3-recommended not found / invalid config shape with default key).
  • Root cause: newer Vue ESLint plugin line expects modern flat-config flow; legacy .eslintrc extends become incompatible.
  • Exact fix: add eslint.config.cjs and migrate rules/plugins there; keep eslint on v9.
  • Preventive rule: treat ESLint plugin major upgrades as config-schema migrations, not package-only updates.
  1. Tailwind v4 upgrader refuses dirty git state by default
  • Symptom: npx @tailwindcss/upgrade aborted with “Git directory is not clean”.
  • Exact fix: run with --force on controlled upgrade branches.
  1. Tailwind upgrader scanned backup directories and failed config linking
  • Symptom: upgrader reported it could not determine config for CSS files in node_modules.bak and resources/css/app.css.
  • Root cause: backup dependency folder was inside repository tree and stylesheet had no explicit config link.
  • Exact fix: move backup folder outside repo and run upgrader with -c tailwind.config.js.
  1. Tailwind upgrader failed to load custom config due legacy helper signature
  • Symptom: Cannot destructure property 'negative' of 'undefined'.
  • Root cause: custom inset: (theme, { negative }) => ... pattern from older Tailwind config API.
  • Exact fix: replace with explicit negative-spacing generation helper that only depends on theme('spacing').
  1. Tailwind template migration failed on @apply of custom class
  • Symptom: Cannot apply unknown utility class 'pl-container'.
  • Root cause: @apply referenced custom class aliases instead of raw utilities in resources/css/utility.css.
  • Exact fix: expand .container rule to raw utility set (pl-8 lg:pl-32 pr-8 lg:pr-32) and rerun upgrader.
  1. Tailwind v4 PostCSS plugin package split
  • Symptom: Vite build failed with “trying to use tailwindcss directly as a PostCSS plugin”.
  • Root cause: Tailwind v4 moved PostCSS integration to @tailwindcss/postcss.
  • Exact fix: install @tailwindcss/postcss and update postcss.config.js plugin key.
  1. eslint-plugin-tailwindcss is not Tailwind v4 compatible
  • Symptom: linting failed with ERR_PACKAGE_PATH_NOT_EXPORTED for tailwindcss/resolveConfig.
  • Root cause: current plugin line peers against Tailwind ^3.4.0 only.
  • Exact fix: remove eslint-plugin-tailwindcss integration from active lint config during Tailwind v4 migration.
  1. Clean npm install exposed missing direct dependency (lodash)
  • Symptom: Vite build failed resolving lodash imported by resources/js/utilities.js.
  • Root cause: package was previously present transitively in old lock state but not declared directly.
  • Exact fix: add lodash as direct dependency.
  • Preventive rule: after lock regeneration, treat unresolved imports as missing direct dependency declarations and fix explicitly.
  1. Filament schema traversal can access uninitialized component container
  • Symptom: runtime crash Typed property Filament\\Schemas\\Components\\Component::$container must not be accessed before initialization while resolving block schema metadata.
  • Root cause: direct child-component traversal (getChildComponents()/getName()) can touch component internals before Livewire/Filament container initialization.
  • Exact fix: in cms/Filament/Blocks/Block.php, resolve protected childComponents via reflection, treat closure child schemas as empty in non-Livewire context, and guard getName() with Throwable fallback to recursive traversal.
  • Preventive rule: when extracting schema metadata outside a mounted form lifecycle, avoid direct getChildComponents() assumptions and handle closure-based schema definitions safely.

Command Transcript (Key Commands)

  1. COMPOSER_CACHE_DIR=/tmp/composer-cache composer outdated --direct --format=json
  2. composer why-not laravel/framework ^12 -t
  3. Multiple Composer major updates (Laravel/Filament ecosystem, plugin replacements, package removals)
  4. npm outdated --json
  5. npm update

npm install @inertiajs/vue3@^2 @vitejs/plugin-vue@^6 laravel-vite-plugin@^2 vite@^7 @vueuse/core@^14 swiper@^12 vue-i18n@^11 pinia@^3 imagetools-core@^9 vite-imagetools@^9 @formkit/auto-animate@^0.9 @gtm-support/vue-gtm@^3

  1. npm install uuid@^13
  2. npm install qs-esm
  3. php artisan test --compact
  4. vendor/bin/pint --dirty
  5. npm run build
  6. vendor/bin/filament-v5 cms
  7. vendor/bin/filament-v5 commerce
  8. vendor/bin/filament-v5 support
  9. Repository-wide Filament import existence scan against Composer autoload
  10. php artisan tinker --execute="Cms\\Filament\\Form\\Fieldset\\ImageForBlock::make('image'); echo 'ok';"
  11. php artisan filament:upgrade --no-interaction
  12. vendor/bin/pint --dirty
  13. php artisan test --compact
  14. npm run build
  15. composer outdated --direct --format=json
  16. npm outdated --json
  17. php artisan route:list --path=admin (fails due constructor abort; see exception #16)
  18. php artisan about

npm pkg delete dependencies.@typescript-eslint/eslint-plugin dependencies.eslint-plugin-unused-imports dependencies.eslint-plugin-vue

  1. npm dependency graph regeneration for ESLint package upgrades
  2. Added flat ESLint config (eslint.config.cjs) and migrated rules from .eslintrc.json
  3. npx @tailwindcss/upgrade --force -c tailwind.config.js
  4. npm install -D @tailwindcss/postcss
  5. npm install lodash
  6. Verification reruns: npx eslint ..., npm run build, php artisan test --compact, composer outdated --direct --format=json, npm outdated --json
  7. php artisan test tests/Feature/Cms/Blocks/BlockSchemaTest.php --compact
  8. Added regression coverage in tests/Feature/Cms/Blocks/BlockSchemaTest.php

npm Final State

Upgraded majors completed in this pass

  • @inertiajs/vue3 -> ^2.3.13
  • @vitejs/plugin-vue -> ^6.0.4
  • laravel-vite-plugin -> ^2.1.0
  • vite -> ^7.3.1
  • @vueuse/core -> ^14.2.1
  • pinia -> ^3.0.4
  • swiper -> ^12.1.0
  • vue-i18n -> ^11.2.8
  • imagetools-core -> ^9.1.0
  • vite-imagetools -> ^9.0.2
  • @formkit/auto-animate -> ^0.9.0
  • @gtm-support/vue-gtm -> ^3.1.0
  • uuid -> ^13.0.0
  • qs-esm added for Ziggy v2 compatibility
  • tailwindcss -> ^4.1.18
  • @tailwindcss/postcss added for Tailwind v4 PostCSS integration
  • @typescript-eslint/eslint-plugin -> ^8.55.0
  • @typescript-eslint/parser -> ^8.55.0
  • eslint-plugin-unused-imports -> ^4.4.1
  • eslint-plugin-vue -> ^10.7.0
  • ESLint migrated to flat config with eslint ^9.39.2
  • lodash added as explicit direct dependency

Remaining npm outdated entries

  • eslint 9.39.2 -> 10.0.0 (left intentionally due current @typescript-eslint v8 peer range support targeting eslint 8/9)

Post-Upgrade Verification

  • composer outdated --direct --format=json: no direct outdated packages ("installed": []).
  • php artisan test --compact: 16 passed (28 assertions) (includes new block schema regression coverage).
  • npm run build: successful client + SSR build on Vite 7.
  • vendor/bin/pint --dirty: passes after fixups.
  • php artisan filament:upgrade --no-interaction: succeeds and republishes Filament assets/caches.
  • php artisan route:list --path=admin: fails because a constructor-level abort is triggered (documented in exception #16).
  • npx eslint resources/js/app.js resources/js/ssr.js: passes with flat ESLint config.

Historical Lessons (From Earlier Upgrade Logs)

  • Source consulted: UPGRADE-LOG-CLEANSHOPPING.md.
  • Stale bootstrap/cache/packages.php and bootstrap/cache/services.php can retain removed service providers after package changes.
  • Filament major upgrades can require running upgrade tooling for custom roots (app/ and modules/).
  • Filament upgrade scripts may miss manual fixes; always run targeted grep and tests after codemods.
  • Tailwind major migrations can fail with advanced config/plugin patterns; run as a dedicated pass.
  • Filament codemod success output is not sufficient validation: cross-check every custom code root and run class-import existence scans.

Reusable Rules

  • Keep this file as the canonical UPDATE_LOG for each future upgrade pass.
  • For each exception, always record:
    • Symptom
    • Failing command
    • Root cause
    • Exact fix
    • Preventive rule

Pending TODOs

  1. Migrate remaining PHPUnit-style tests to idiomatic Pest tests
    • Reference guide: https://pestphp.com/docs/migrating-from-phpunit-guide
    • Scope: convert class-based PHPUnit style tests (tests/Feature/*, tests/Unit/*) to Pest it() style where practical.
    • Keep/validate safety guard before every migration test run: tests/TestCase.php must enforce in-memory sqlite ( database.default=sqlite and database.connections.sqlite.database=:memory:) to prevent accidental writes to non-test databases.

Session Notes — 2026-02-10 (Browser Testing Bootstrap)

  1. Composer dev-package install can rerun Filament upgrade hooks

    • Symptom: installing browser-test dependencies triggered php artisan filament:upgrade via Composer scripts, republishing Filament assets and clearing caches.
    • Failing command: composer require pestphp/pest-plugin-browser --dev --no-interaction.
    • Root cause: project-level post-update-cmd in composer.json always runs @php artisan filament:upgrade.
    • Exact fix: treat as expected side effect and avoid repeated Composer writes when not needed.
    • Preventive rule: on upgrade branches, assume Composer package changes may create large Filament asset diffs.
  2. Pest browser tests need Playwright runtime setup in addition to npm package install

    • Symptom: browser tests can fail on clean machines if Playwright browser binaries are not installed.
    • Failing command: ./vendor/bin/pest tests/Browser/... before Playwright install.
    • Root cause: installing playwright npm package does not guarantee browser binaries are present.
    • Exact fix: run npx playwright install (or npx playwright install chromium) after dependency install.
    • Preventive rule: include Playwright browser install in local + CI bootstrap for browser test suites.
  3. Pest v4 browser tests conflict with public setUp() in custom TestCase

    • Symptom: fatal error Access level to Pest\Concerns\Testable::setUp() must be public (as in class Tests\TestCase).
    • Failing command: ./vendor/bin/pest tests/Browser/PageBrowserTest.php.
    • Root cause: tests/TestCase.php used public function setUp(), which conflicts with Pest's trait method visibility expectations.
    • Exact fix: change tests/TestCase.php setUp() visibility to protected.
    • Preventive rule: keep custom setUp() visibility aligned with Laravel/PHPUnit defaults (protected) for Pest compatibility.
  4. Browser title assertions can be empty on Inertia pages during early render

    • Symptom: browser test failed with Expected page title ... but found '' on a valid Inertia route.
    • Failing command: ./vendor/bin/pest tests/Browser/PageBrowserTest.php.
    • Root cause: in this stack, <title> can be empty at initial render timing while the page payload is already present.
    • Exact fix: assert against rendered source payload (assertSourceHas(...)) plus smoke assertions instead of strict immediate title assertion.
    • Preventive rule: for Inertia browser tests, prefer stable assertions (assertSourceHas, assertSee, assertNoSmoke) over immediate <title> checks unless waiting for a client-side update.
  5. Block settings detection can still trigger uninitialized Filament container access

    • Symptom: browser request crashed with Typed property Filament\\Schemas\\Components\\Component::$container must not be accessed before initialization.
    • Failing path: Cms\\Filament\\Blocks\\Block::hasSettings() calling getSettings()->getChildComponents() during content arraying.
    • Root cause: hasSettings() used a direct child-component accessor that requires a mounted container, even in non-Livewire rendering.
    • Exact fix: change hasSettings() to use the safe getSchemaChildComponents() resolver and treat closure schemas as non-resolvable (false) in this context.
    • Preventive rule: in backend-only schema introspection, avoid direct getChildComponents() calls and rely on guarded reflection/fallback accessors.
  6. Pest browser assertSee() can fail on non-SSR Inertia pages

    • Symptom: browser test did not find seeded text as visible DOM text even though the route and payload were correct.
    • Failing assertion: assertSee('Browser Seed Content').
    • Root cause: this page flow is client-rendered Inertia; text is present in response payload/source before hydration, not necessarily as immediate visible server-rendered DOM.
    • Exact fix: use assertSourceHas(...) for payload-level assertions plus route/smoke assertions.
    • Preventive rule: for non-SSR Inertia browser tests, prefer assertSourceHas over immediate assertSee unless explicitly waiting for frontend hydration.

Session Notes — 2026-02-10 (Remaining Update Pass)

  1. Final direct Composer updates are complete

    • Action: upgraded remaining direct outdated packages (filament/filament, filament/spatie-laravel-media-library-plugin, filament/spatie-laravel-settings-plugin, filament/upgrade) from 5.2.0 to 5.2.1.
    • Verification: composer outdated --direct --format=json now returns "installed": [].
  2. Composer update still triggers Filament asset republish side effects

    • Symptom: composer update ... executed post-update-cmd and reran php artisan filament:upgrade, republishing public/js/filament/*, public/css/filament/*, and font assets.
    • Impact: expected large public asset churn in git diff.
    • Preventive rule: keep Filament public-asset changes grouped and reviewed separately when running Composer updates.
  3. npm is fully up to date except ESLint major

    • Action: ran npm update and refreshed lockfile.
    • Result: only eslint remains in npm outdated (9.39.2 -> 10.0.0).
    • Decision: keep on ESLint 9 for this pass to avoid forced major migration risk while finishing framework/package updates.
  4. Build + tests remain green after patch pass

    • npm run build: successful (client + SSR).
    • php artisan test --compact: 16 passed (28 assertions).
    • ./vendor/bin/pest tests/Browser/PageBrowserTest.php: passes.
    • php artisan test tests/Feature/Cms/Blocks/BlockSchemaTest.php --compact: passes.
    • vendor/bin/pint --dirty: passes.
  5. Tailwind v4 can silently reintroduce core .container and override custom layout containers

    • Symptom: major frontend layout degradation after upgrade (page spacing/width looked "missing CSS" across many pages).
    • Root cause: Tailwind v4 no longer supports corePlugins.container = false from JS config in active CSS-first mode; the core .container utility was generated and overrode project custom .container.
    • Exact fix: in resources/css/app.css, add @source not inline('container'); (and @source not inline('container!');) to prevent Tailwind from generating the core container utility, keeping the custom .container from resources/css/utility.css as the only definition.
    • Verification: built CSS now contains only the custom .container rule (grep -o '\\.container{' ... | wc -l = 2 from base + media rule), browser + schema tests pass.
    • Preventive rule: after Tailwind major upgrades, always diff/check compiled .container output when projects define their own container class name.
  6. Tailwind v4 does not require tailwind.config.js, but compatibility file can remain accidentally

    • Symptom: repository still had tailwind.config.js after migration, creating ambiguity about active config source.
    • Root cause: Tailwind v4 uses CSS-first config; JS config is compatibility-only and not auto-loaded unless @config is explicitly used.
    • Exact fix: remove unused tailwind.config.js when fully migrated to CSS directives and no tooling references remain.
    • Preventive rule: after v4 migration, run a repo-wide search for @config and tailwind.config.js references; if none exist, remove stale config files to avoid future confusion.

Tailwind Upgrade Workflow Directive (Authoritative)

  1. Always run the Tailwind upgrade utility first

    • Required default path: use the official updater before doing manual migrations.
  2. If the upgrade utility errors, resolve blockers and rerun the utility

    • Do not switch to a manual full migration as first fallback.
    • Iterate: fix error -> rerun updater -> repeat until updater succeeds.
  3. If custom classes block the updater, temporarily comment them out

    • Typical case: custom utility/class patterns causing updater parse/apply failures.
    • Workflow: comment problematic custom classes -> run updater -> reapply custom classes after successful upgrade.
  4. Manual edits are only for unblocking/reapplying around the updater

    • Manual work should support the upgrader workflow, not replace it.
  5. Unpack all custom classes before running Tailwind upgrades

    • Rule: do not keep "custom class inside custom class" patterns (for example, a custom class that @applys another custom class).
    • Required approach: expand nested custom-class usage to raw utility classes first, then run the upgrader.
    • After upgrade: keep this flattened structure; avoid reintroducing custom-class-in-custom-class composition.
    • Verification snapshot (2026-02-10): repository-wide @apply scan found only raw utility usage in resources/css/app.css; no custom-class tokens were referenced inside other custom classes.
  6. Blade-injected runtime colors require @theme inline in Tailwind v4

    • Symptom: color utilities tied to runtime CSS variables from resources/views/app.blade.php behaved inconsistently after migration.
    • Root cause: theme tokens in resources/css/app.css referenced runtime vars (var(--primary-color) etc.) using plain @theme instead of v4's recommended @theme inline for variable references.
    • Exact fix: switch to @theme inline for color tokens and ensure every referenced token exists.
    • Verification snapshot (2026-02-10): compiled CSS now emits .bg-primary{background-color:var(--primary-color)} and .from-primary-medium{...var(--primary-color)...}.
    • Preventive rule: when colors are injected at runtime (Blade/style attributes), define Tailwind color tokens with @theme inline.
  7. Missing theme token prevents utility generation (from-primary-medium)

    • Symptom: from-primary-medium class in resources/js/Components/Header.vue had no compiled CSS.
    • Root cause: no --color-primary-medium token existed in the Tailwind theme namespace.
    • Exact fix: add --color-primary-medium mapping in the @theme inline block (resources/css/app.css).
    • Preventive rule: after migration, grep for custom color utility names and verify each has a corresponding --color-* theme variable.