Barrels and internals
SkillFiles & storageA new-architecture package (shared/, domain/, features/) exposes its API through barrels that contain nothing but `export *`, and keeps its private code in an internals location. Read this when creating a package under shared/, domain/ or features/, when editing any `index.*` file, or when a `lint:structure` check fails.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Barrels and internals skill
What this skill tells your AI
The instructions your AI receives, as published by ledgerhq/ledger-live in .agents/skills/package-public-api/SKILL.md and read by ahel’s review.
An index.* file is a barrel: a pure regrouping point, nothing else.
// ✅
export * from "./schema";
export type * from "./types";
export { default } from "./useEnv"; // the one tolerated exception
// ❌ sorting in the export proves ./slice mixes public and private
export { accountNamesSlice, bulkSetAccountNames as setAccountNames } from "./slice";
// ❌ an index that contains code is not an index
export const CurrencySchema = z.discriminatedUnion("type", [...]);
Why the sorting matters. If you need to pick which names leave a file, that file holds both
public and private code. The fix is never a longer export list — it is moving the private part to
an internals location. export * then becomes honest: everything in that file is public.
Enforced by nx run <project>:lint:structure
(the plugin), on every index.* under
src/, at any depth.
Where private code goes
Three recognised forms, usable at any depth:
| Form | Use for |
|---|---|
internals/ | a private area with several files |
internals.ts | one private helper in a folder |
<name>.internals.ts | the private half of a file, e.g. slice.ts + slice.internals.ts |
A barrel must never re-export any of them — that would make private code public and defeat the
point. Everything else can: a test sitting next to slice.internals.ts imports it directly, so
private code stays fully testable without being part of the package API.
Never re-export another package
// ❌ this package becomes a proxy
export * from "@features/flow-contacts-add-contact";
export { useContacts } from "@features/platform-contacts";
Two import paths for the same symbol, and a reader who cannot tell who provides it. Consumers must
import the original provider, and declare the dependency themselves. This holds for @shared/*,
@domain/*, @features/*, @support/* and @ledgerhq/* alike.
Facade is not proxy. Wrapping a legacy lib behind a typed API — as shared/env does over
@ledgerhq/live-env — is legitimate: it owns real code and holds a declared
BOUNDARY_EXCEPTION. Keeping that
wrapping in a named file rather than the barrel is the cheapest way to stay conformant; shared/env
instead carries a temporary skip in
exceptions.js. Entries there
are team decisions with an exit condition, not a way around a failing check — never add one to make
your own package pass.
Traps
export *does not propagate a rename.export { bulkSetAccountNames as setAccountNames }cannot becomeexport *. Rename at the source and update the callers, so one thing has one name.export *does not propagate a default either. Only the re-export form (export { default } from "./x") is tolerated in a barrel. A localexport default myThingneeds animportabove it, which is code in an index — extract it.- A package can have several barrels. With a
react-nativecondition inexports, bothindex.tsandindex.native.tsare barrels and both must stay pure. - An
index.*holding code is not an index. Move the code to a named file (useEnv.ts,schema.ts,CardLogin.web.tsx) and leaveexport * from "./thatFile"behind. Import paths for consumers do not change. - A folder holding only
index.tsshould not be a folder.definitions/team-qaa/index.tsbecomesdefinitions/team-qaa.ts: same import specifier, one less file, no barrel to keep pure.
New package
Start conformant, it costs nothing:
src/
├── index.ts # export * from "./schema";
├── schema.ts
├── schema.test.ts
└── internals.ts # whatever schema.ts should not expose
See ddd-structure-flow for where the package itself belongs.
Reviewing
Refuse and point here when you see:
- a named re-export (
export { a, b } from "./x") in anyindex.* - an
import, a declaration or a localexport defaultin anyindex.* export * as ns from— namespacing is still sorting- a barrel re-exporting
internals, or re-exporting another workspace package
Signals
- GitHub stars
- 618
- Forks
- 490
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
package-public-api- Source
- github.com/ledgerhq/ledger-live