AI agent skill · by @LedgerHQ
Package Public Api
A 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.
Archived 2026-10-23 · Source updated 2026-10-12 · License: MIT. Check the original repository for the latest version.
SKILL.md instructions
# Barrels and internals
An `index.*` file is a **barrel**: a pure regrouping point, nothing else.
```ts
// ✅
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](../../../tools/nx-plugins/enforce-package-structure/)), 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
```ts
// ❌ 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`](../../../tools/nx-plugins/enforce-boundaries/constraints.js). 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`](../../../tools/nx-plugins/enforce-package-structure/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 become `export *`. 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 local `export default myThing`
needs an `import` above it, which is code in an index — extract it.
- **A package can have several barrels.** With a `react-native` condition in `exports`, both
`index.ts` and `index.native.ts` are 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 leave `export * from "./thatFile"` behind.
Import paths for consumers do not change.
- **A folder holding only `index.ts` should not be a folder.** `definitions/team-qaa/index.ts`
becomes `definitions/team-qaa.ts`: same import specifier, one less file, no barrel to keep pure.
## New package
Start conformant, it costs nothing:
```text
src/
├── index.ts # export * from "./schema";
├── schema.ts
├── schema.test.ts
└── internals.ts # whatever schema.ts should not expose
```
See [structure-flow](../structure-flow/SKILL.md) for where the package itself belongs.
## Reviewing
Refuse and point here when you see:
- a named re-export (`export { a, b } from "./x"`) in any `index.*`
- an `import`, a declaration or a local `export default` in any `index.*`
- `export * as ns from` — namespacing is still sorting
- a barrel re-exporting `internals`, or re-exporting another workspace package