ApexLibrarian
A librarian for your Apex classes — it picks the books up off the floor and puts them back on their shelves.
Salesforce orgs have no concept of folders for Apex classes. Your repository does — Usecases/opportunity/, Handlers/account/, tests/ — but the moment a class travels through an org (a teammate creates it in Setup, you retrieve it into another repo, you install it as a package and pull the source), that layout is stripped and the file lands flat in classes/.
ApexLibrarian is an npm CLI that tunnels the layout through the org by declaring it inside the class itself, in the doc comment:
/**
* Recalculates opportunity discounts from line items.
* @directory Usecases/opportunity
*/
public with sharing class ReconcileDiscount {
Doc comments are part of the class body, so the @directory tag survives every round trip. After a retrieve, one command shelves everything where it belongs.
Install
npm install --save-dev apex-librarian
# or run it directly
npx apex-librarian check
Pin it as a devDependency: a tool that moves files should behave identically for every teammate and in CI.
Commands
The tag is the declaration, the filesystem is the reality. Each command resolves divergence in one direction:
| Command | Direction | Use it when |
|---|---|---|
apex-librarian check | compare only | CI / pre-commit — exits 1 on any divergence |
apex-librarian arrange | tag → location | after sf project retrieve — moves files (and their .cls-meta.xml) to the tagged directory |
apex-librarian stamp | location → tag | adopting an existing project, or after a deliberate git mv |
Options
--root <dir> class root to operate on (repeatable; default: every "classes" directory found)
--dry-run print what would happen without touching any file
--require-tag check only: untagged classes in subdirectories also fail
--include-submodules
stamp/check: also operate on files inside git submodules (see "For framework authors")
Behavior notes
.cls-meta.xmlalways moves together with its class.- Git submodules are excluded automatically (paths from
.gitmodules). A vendored framework's layout is upstream's business — the librarian never moves or stamps files inside one. arrangenever overwrites: an occupied target is reported and skipped (exit 1).- The tag binds to the doc block attached to the top-level type declaration. License headers at the top of the file don't count; inner classes are ignored.
@directory .means the class root itself. Absolute paths and..are rejected.- Untagged classes are left alone by
arrangeandcheck(add--require-tagto enforce tagging);stampadds tags to classes already living in subdirectories. - Run it on a committed working tree when you can — every move is a plain rename, so
git statusshows exactly what the librarian did, andgit checkout .undoes it.
Typical flows
Adopting an existing project
Write the current layout into every class once:
npx apex-librarian stamp
git add -A && git commit -m "Stamp @directory tags"
After a retrieve
New classes created by teammates in the org arrive flat; shelve them:
sf project retrieve start -o my-org ...
npx apex-librarian arrange
After moving a class on purpose
Tell the tag about it:
git mv force-app/main/default/classes/Foo.cls force-app/main/default/classes/Services/
npx apex-librarian stamp
CI example (GitHub Actions)
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx apex-librarian check --require-tag
For framework authors
If you publish a framework as an unlocked package, consumers who retrieve your classes get them flat — the org strips all layout. Bake the destination into your source instead. Work from a checkout where your framework is installed the way you intend consumers to place it — as a git submodule under classes/<YourFramework>/ — and stamp the real hierarchy as-is:
npx apex-librarian stamp --include-submodules
npx apex-librarian check --include-submodules # verify
Every class now carries its actual location, e.g. @directory MyFramework/Eloquents/tests. Commit those tags in the submodule and release. A consumer who installs your package and retrieves the source just runs npx apex-librarian arrange — and your classes shelve themselves under classes/MyFramework/..., exactly where a submodule install puts them. Same layout on every install path, declared by the classes themselves.
--include-submodules is a separate flag because submodule paths are excluded by default: an ordinary consumer must never re-stamp or move a vendored framework's files. arrange refuses the flag outright — moving a file out of a submodule's working tree would corrupt the submodule.
All four Apex Stem frameworks ship with these tags (as of ApexEloquent v3.7.0 / ApexBlueprint v2.1.0 / ApexTrace v1.5.0 / ApexTools v1.1.0) — see the installation guide.
Links
- GitHub: krile136/ApexLibrarian
- npm: apex-librarian
- Library usage: everything the CLI does is exported from
apex-librarianas plain functions (analyze,planArrange,applyMoves, ...), so it can be embedded in other tooling