Skip to content

Upgrading Dependencies

Dependency upgrades are part of normal maintenance, but not all upgrades should be treated the same way.

  • Patch and minor updates are usually routine.
  • Major updates are deliberate engineering work.
  • The important distinction is not only "how do we install updates?" but also "how do we notice versions that our current constraints intentionally do not allow?"

This project uses two ecosystems:

  • Backend: Composer in backend/composer.json
  • Frontend: vp as the primary toolchain interface, backed by Bun-managed dependencies in frontend/package.json, with a root-level vite-plus helper pin in package.json for workspace commands such as vp run ... and vp config

For the frontend, vp is the command surface we use day to day:

  • vp owns the dev/build/test/lint/package-management command surface (vp dev, vp build, vp check, vp test, vp install, vp update, vp outdated).
  • Bun still owns the underlying package resolution and lockfile format through vp.

Most direct dependencies use SemVer ranges such as ^13 or ^5.2. That is good for stability, but it also means composer update and frontend package-manager updates will usually stop at the newest version inside the current major line. They do not automatically move us to a new major version.

Upgrade Tiers

TypeExampleUsual handling
Patch1.2.3 -> 1.2.4Routine update
Minor1.2 -> 1.3Routine update with full verification
Major1.x -> 2.0Planned upgrade branch

Even patch and minor updates can break in practice, so every upgrade should be followed by the relevant tests and checks.

Execution Modes

There are two useful ways to run this protocol:

  • Rehearsal mode: non-mutating checks that prove the process works before touching lockfiles.
  • Real update mode: actually change dependencies, lockfiles, and any generated artifacts triggered by package-manager hooks.

Use rehearsal mode first when you want a safe signal about whether the current branch is healthy enough for upgrade work.

Rehearsal Mode

bash
cd frontend && vp update -- --dry-run
cd backend && composer update --dry-run

Then run the same verification suite you would use after a real update.

Routine Updates

Frontend

These commands are mutating. In this repo, vp update updates the installed dependency graph, refreshes frontend/bun.lock, and may also rewrite version ranges in frontend/package.json to the newer resolved releases.

Run from the repository root or from frontend/:

bash
cd frontend
vp update

Review both frontend/package.json and frontend/bun.lock after the update instead of assuming the manifest stayed unchanged.

If the goal is specifically to refresh the frontend toolchain itself, check the Vite+ packages too:

bash
cd frontend
vp update vite-plus vite vitest

If you intentionally change the Vite+ toolchain version, also sync the root package.json helper pin (vite-plus plus the root vite and vitest overrides) so workspace-level vp run ... and vp config stay aligned with the frontend.

Then verify:

bash
vp check
vp test

Backend

These commands are mutating. They may also trigger Composer post-update hooks that regenerate published assets.

Run from backend/:

bash
cd backend
composer update

In this repo, composer update runs Laravel and Filament post-update hooks. Even when the lockfile does not change, those hooks can republish generated assets under backend/public/. Review those diffs separately from actual dependency changes.

Then verify:

bash
php artisan test --parallel
composer phpstan
composer deptrac

If php artisan test --parallel fails locally with PostgreSQL errors like out of shared memory or max_locks_per_transaction, treat that as a local database-capacity issue first, not automatic evidence of an upgrade regression. For upgrade debugging, either raise the Postgres limit or rerun the backend tests without parallelism to separate environment pressure from application breakage.

How To Detect New Major Versions

This is the part people often miss.

When a dependency is constrained with ^, the package manager will usually not upgrade across a major boundary during a normal update. So we need a separate "major scan".

Composer

Composer has first-class support for this:

bash
cd backend
composer outdated --direct --major-only

This shows direct dependencies where a newer major exists.

Useful variants:

bash
composer outdated --direct
composer outdated --direct --format=json

Interpretation:

  • composer update answers: "What can we safely install within current constraints?"
  • composer outdated --major-only answers: "What new major lines exist beyond our current constraints?"

Composer Transitive Dependencies

Sometimes composer outdated surfaces a package that we do not require directly. Treat that as a dependency-graph question, not an automatic upgrade task.

Recommended check order:

bash
cd backend
composer audit
composer why brick/math
composer prohibits brick/math 0.17.1

Interpretation:

  • If composer audit is clean, there is usually no urgency.
  • composer why shows which direct or transitive packages pull the dependency into the graph.
  • composer prohibits shows which package is blocking a specific target version.

Do not add a transitive package to composer.json just to force a newer version unless we have a concrete reason, such as a security advisory, a bug we are actually hitting, or a direct feature/API need.

Example: brick/math 0.14.8 may appear outdated while still being the correct resolved version. In this repo, Laravel allows newer brick/math, but ramsey/uuid 4.9.2 still limits it to ^0.14, so trying to force brick/math 0.17.1 creates solver friction without practical benefit.

Frontend (vp)

vp does not have a dedicated --major-only flag like Composer, but vp outdated gives the information we need:

bash
cd frontend
vp outdated

The output includes:

  • Current: installed version
  • Update: newest version allowed by the current range
  • Latest: newest version published

After the Vite+ migration, pay particular attention to the toolchain packages in frontend/package.json:

  • vite-plus
  • vite (aliased to @voidzero-dev/vite-plus-core)
  • vitest (aliased to @voidzero-dev/vite-plus-test)

And remember that the repository root also carries a vite-plus helper pin plus vite/vitest overrides in package.json. That root file does not drive the SPA build itself, but it does affect root-level vp workflows.

Interpretation:

  • If Update and Latest are the same, your current range can already reach the latest release.
  • If Update is behind Latest, a newer version exists outside your current allowed range.
  • In practice, that often means a new major is available and your ^ range is intentionally blocking it.

Example:

text
| Package       | Current | Update  | Latest  |
| lucide-react  | 0.562.0 | 0.562.0 | 0.577.0 |

That means a newer release exists, but vp update will not take it with the current constraint.

Use two different rhythms:

1. Routine dependency maintenance

Do this regularly:

  • cd frontend && vp update
  • cd backend && composer update
  • Run the normal verification suite

2. Major-version scan

Do this on a schedule, for example once a month:

bash
cd frontend && vp outdated
cd backend && composer outdated --direct --major-only

This keeps major upgrades visible without forcing them into every routine maintenance pass.

Major Upgrade Process

When we decide to take a major version, treat it as a small project.

Principles

  • Upgrade one major dependency at a time when feasible.
  • Do not combine unrelated major upgrades in one branch unless there is a strong reason.
  • Read the official upgrade guide before changing code.
  • Start from a clean baseline with passing tests and analysis.
  • Prefer small, reviewable commits over one giant "upgrade everything" diff.

Workflow

1. Read the upstream guide

Examples:

2. Establish a clean baseline

Do the safe rehearsal first so you know whether the branch is already unstable before you mutate anything.

bash
# Backend
cd backend
php artisan test --parallel
composer phpstan
composer deptrac

# Frontend
cd ../frontend
vp update -- --dry-run
vp check
vp test

If the baseline is already broken, fix that first. Upgrade work is much harder to reason about on top of existing failures.

3. Create a dedicated branch

Example:

bash
git checkout -b upgrade/filament-6

4. Upgrade the dependency explicitly

Examples:

bash
# Composer
cd backend
composer require filament/filament:^6.0 --update-with-dependencies

# Bun
cd ../frontend
vp add some-package@1.2.3

For frontend packages, prefer an explicit target version instead of blindly taking latest, especially when the toolchain is still evolving quickly.

For Vite+ toolchain upgrades, prefer explicit package names so the aliasing stays obvious in review:

bash
cd frontend
TARGET_VP=0.1.23
vp add -d vite-plus@"$TARGET_VP" vite@npm:@voidzero-dev/vite-plus-core@"$TARGET_VP" vitest@npm:@voidzero-dev/vite-plus-test@"$TARGET_VP"

Then mirror the same target version in the root package.json helper pin and overrides before reviewing the combined lockfile diff.

After the real update command finishes, inspect lockfile and generated-asset diffs before assuming every changed file represents a real behavioral change.

5. Fix breakages iteratively

Suggested order:

  1. Static analysis and type checks
  2. Automated tests
  3. Manual smoke testing of the affected flows

6. Regenerate artifacts when relevant

If backend API signatures changed:

bash
cd frontend
vp run api:generate

7. Document what changed

If the upgrade taught us project-specific lessons, add them to this document so the next upgrade starts from real local knowledge, not memory.

What Not To Assume

  • A normal update command does not mean "we are fully up to date".
  • A green lockfile refresh does not mean "no major upgrades exist".
  • SemVer helps, but it does not remove the need for tests.
  • Some ecosystems, especially frontend tooling, occasionally ship breaking behavior in minor releases. Verify, do not trust blindly.

Version History

Routine Composer and frontend refresh (July 2026)

This was a routine in-range dependency refresh, not a major-version upgrade.

Local lessons:

  • Newer Larastan reports env() calls outside config/ with larastan.noEnvCallsOutsideOfConfig; move those values into config keys and read them with config() from providers, helpers, services, and controllers.
  • Newer Vite+/Vitest mock handling is stricter about missing mocked exports and hoisting. If a module imports more of a mocked package, update the mock to expose those exports, and use vi.hoisted() for state referenced by a hoisted vi.mock() factory.
  • Full frontend test runs may time out under heavy parallel load even when the affected files pass directly. Rerun the listed failing files before assuming the upgrade introduced deterministic regressions.

PHP 8.2 -> 8.5 and Filament 3.1 -> 5.0 (February 2026)

This was completed together, which worked, but is not the preferred pattern for future upgrades.

Main breakage areas:

  • Filament resource and page APIs changed significantly between v3 and v5: Form -> Schema, actions moved namespaces, and layout/schema components shifted
  • SpatieLaravelTranslatablePlugin was replaced by LaraZeus\\SpatieTranslatable\\SpatieTranslatablePlugin for Filament v5 compatibility
  • filament-shield, filament-users, and filament-impersonate all required major-version alignment with Filament itself
  • PHPUnit major upgrades tightened some mocking behavior and required test updates; the first sharp edge in this repo showed up during 11 -> 12, and the current line is now ^13.1
  • PHP 8.5 itself was relatively smooth thanks to existing static analysis coverage

Current Versions

DependencyVersion
PHP^8.5
Laravel^13.0
Filament^5.2
PHPUnit^13.1
React^19.2
Vite+ (frontend toolchain)0.1.23
Vite+ (root helper pin)0.1.23
TypeScript~6.0