stolt/lean-package-validator
A CLI utility that validates a project or micro-package for leanness, ensuring common repository artefacts are excluded from release assets. It can also create, update, and reformat the leanness-enforcing export-ignore entries of a .gitattributes file.
This package is for the authoring side of Laravel packages, not for the Laravel app itself. Reach for it when you're building a package that ships as a Composer dist archive (which is the default install path for most consumers), and you want to make sure tests, CI configs, PHPStan configs, editor dotfiles, and Pint/PHPUnit caches don't end up inside that archive.
Minimal steps from your package root:
composer require --dev stolt/lean-package-validator
./vendor/bin/lean-package-validator validate
If validation fails, the fastest path to diagnosis is validate --diff, which renders a unified diff of your current .gitattributes versus the expected content. From there you either run create (no .gitattributes yet), update (one exists but is stale), or reformat (content is correct but layout is messy). All three support --dry-run where it makes sense — update and reformat do; use them first.
Where to look first:
validate section of the README to understand classic vs. negated flavours — the tool validates both, and you should pick one for your repo.src/Presets/PhpPreset.php to see exactly which paths are considered "common repository artefacts" in the default PHP preset. That's the source of truth for what it wants exported-ignored.example/.lpv file if you need to customise the glob pattern beyond the preset.First real use case: a new Laravel package right before your first composer require yourvendor/yourpackage test from a separate app. Run create, commit, tag, then pull the dist tarball down manually to sanity-check. This is where the tool pays for itself — you catch a shipped tests/ directory or a stray phpunit.xml.dist before your first consumer sees it.
For one-off checks you can skip the dev dependency entirely:
cpx stolt/lean-package-validator validate
Guard via a Composer script. This is the pattern the README itself recommends and the one that fits Laravel package workflow best, because it puts the check one command away for every contributor:
{
"scripts": {
"validate-gitattributes": "lean-package-validator validate"
}
}
Then composer validate-gitattributes becomes the thing you put in composer test or a pre-push hook.
Gate CI on the archive, not just the file. validate checks the .gitattributes content; validate --validate-git-archive actually builds a temporary archive from HEAD and inspects it. For a release pipeline, the archive check is the meaningful one — it catches the case where your .gitattributes looks right but something else slipped in. A typical job:
- run: composer install --no-interaction
- run: ./vendor/bin/lean-package-validator validate --validate-git-archive --keep-license
Maintain, don't hand-edit. Once a .gitattributes is created, the update command is the workhorse. When you add a new tool (Pest, Rector, a new cache dir), you don't reason about what to add — you run update --dry-run, eyeball the diff, then run it for real. It preserves non-export-ignore rules and appends new export-ignores below them. reformat is for cosmetic drift: --sort-alphabetically, --sort-from-directories-to-files, --group.
Use .lpv for anything non-vanilla. If you're on the default PHP preset you don't need it. But if your package has unusual paths, generate a .lpv with init and commit it — validate picks it up automatically. When the upstream PHP preset grows new entries in a later release, refresh adds the missing ones while leaving your manual entries alone. That's the upgrade path: bump the package, run refresh --dry-run, commit, done.
Enforce style consistently or not at all. --enforce-strict-order and --enforce-alignment are opt-in for a reason. If you enable them, make sure everyone's regenerating the file with the tool rather than editing by hand, otherwise every stray update by a teammate with a different sort preference will fail CI. For most teams the non-strict default (order-insensitive) is the right call, with reformat used occasionally to tidy up.
Preset switching for polyglot repos. If you have a Laravel package with a bundled JS build or Rust extension, --preset JavaScript / --preset Rust changes the glob pattern set. Note this is per-invocation unless you carry a .lpv — the preset flag on validate doesn't mutate a .lpv.
.gitattributes affects git archive and Composer's dist archives, not the working tree. A contributor who git clones and installs from source never sees this. It matters only for consumers installing from a dist tarball — which is most of them, so don't skip it.--validate-git-archive operates on HEAD, not your working directory. Uncommitted changes are invisible to it. In CI that's fine; locally it means you must commit before the check is meaningful, and a green run against an old commit is a false sense of security.--keep-license combined with --validate-git-archive makes a license file mandatory — if you don't have one, validation fails rather than passing leniently. Same idea with --keep-readme. Pick your flags deliberately; adding --keep-license to satisfy a legal obligation and then forgetting it in one job is a subtle way to get inconsistent CI.phpstan.neon.dist but leave its export-ignore line, plain validate still passes. Add --report-stale-export-ignores (and ideally --diff) to surface them.create --force will overwrite an existing .gitattributes. If that file holds non-export-ignore rules (line-ending normalisation, *.php text eol=lf, binary markers), you lose them. Prefer update whenever the file exists.update appends below existing rules and respects --omit-header. If a teammate strips the header in their branch, update without --omit-header will reintroduce it and produce a noisy diff..gitattributes diffs. Decide the policy once.--enforce-alignment is brittle against mixed editor setups (trailing whitespace, tab/space). It's great for a solo repo, annoying for a distributed team. If you enable it, wire reformat --align-export-ignores-style regeneration into the same CI job that validates.--glob-pattern and --glob-pattern-file expect the brace grouping to have more than one pattern. A single-element {foo} will not behave how you expect. Use {.lpv} for anything complex rather than long inline flags.--stdin-input only does a strict comparison — it doesn't apply presets, alignment, or stale reporting. It's fine for a quick pipe check but don't use it as your CI gate.tree --src and tree --dist-package commands are inspection only. They're the fastest way to eyeball what would ship before you tag, but the tool explicitly says they're not for validation — don't script against their output.bin-dir on $PATH; the README's alias recipe (alias lpv='~/.config/composer/vendor/bin/lean-package-validator $@') is worth setting up as the name is long enough that you won't type it otherwise, which is how .gitattributes files go stale in the first place.composer.json must never be export-ignored — the default presets correctly keep it in. If you ever hand-edit and add it, composer require from dist breaks. The validate --validate-git-archive path catches this; plain content validation against a custom glob might not.resources/boost/skills) document validate/update/create flows in agent-friendly form. If you're using an agent to maintain .gitattributes, point it at those rather than rederiving the workflow.How can I help you explore Laravel packages today?