package-public-api
Skill627 starsChanged 8 days agoMITView on GitHub
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 when creating a package under shared/, domain/ or features/, when editing any `index.*` file, or when a `lint:structure` check fails.
In this repository70 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- analyticsSkill
- batch-feature-devSkill
- ci-runnersSkill
- ci-workflow-refsSkill
- cleanupSkill
- client-idsSkill
- cloud-sync-moduleSkill
- codeownershipSkill
- coin-families-contractSkill
- coin-modulesSkill
- commentsSkill
- configurationSkill
- console-logSkill
- create-changesetSkill
- create-prSkill
- data-layer-advancedSkill
- debug-rn-native-crashSkill
- dependency-patchesSkill
- detect-data-leaksSkill
- device-intent-executorSkill
- devtools-import-boundarySkill
- dialogs-sliceSkill
- docs-migrateSkill
- docs-update-or-createSkill
- domain-dataSkill
- e2e-desktop-add-or-updateSkill
- e2e-desktop-onboardSkill
- e2e-earnSkill
- e2e-mobile-add-or-updateSkill
- e2e-mobile-onboardSkill
- e2e-swapSkill
- errorsSkill
- feature-devSkill
- fix-ciSkill
- flow-packageSkill
- get-pr-commentsSkill
- git-conventionsSkill
- i18n-keysSkill
- impacting-prsSkill
- knip-migrationSkill
- ldls-nativeSkill
- ldls-webSkill
- ledger-wallet-cliSkill
- legacy-migrationSkill
- mvvm-architectureSkill
- new-packageSkill
- pnpm-resolutionSkill
- prefer-set-hasSkill
- pre-reviewSkill
- react-generalSkill
- rebuild-to-fix-lib-import-errorsSkill
- remove-feature-flagSkill
- run-buildsSkill
- run-e2e-ciSkill
- runner-imagesSkill
- run-testsSkill
- slack-pr-messageSkill
- The first 60 shown.
The file
We split this skill into the rules it gives an agent
4 rules. About 0 are common, 3 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 27
Why the sorting matters: If you need to pick which names leave a file, that file holds both public and private code.
- Very specific: line 79
No: delete the barrel and import the files directly, or move the folder under internals/.
- Distinctive: line 150
Import paths for consumers do not change.
- Distinctive: line 156
Start conformant, it costs nothing:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Barrels and internals
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.
The rule follows a file's role, not its name. It follows every relative export * from a barrel,
through any number of files, resolved as the web and native bundlers do, and it reads every
source file named in package.json#exports, under any condition. Test support entries (a subpath
segment starting with test or mock) are left out. Such a module may declare
anything, but it re-exports like a barrel: export * from a relative file and an optional default
re-export only. So export * from "./exports" in the index with the sorting or the proxy in
exports.ts fails the same way (hidden-barrel, cross-package-reexport, …).
Where a barrel may sit
Under src/, an index.* (platform variants included) is allowed in three places:
- An entry:
src/index.*, or the target of apackage.json#exportsentry. Test support entries count. - Below an entry, through
export *. One level below an entry is always fine. Deeper, the barrel is a nested barrel andnested-barrelreports it, unless the chain is pure: every barrel on the way holds onlyexport * from "./x"inside its own folder (plain modules on the way do not count). The wording isBARREL_LOCATIONandCHAIN_BREAKSinrules.js.<!-- rules.js: BARREL_LOCATION, CHAIN_BREAKS --> - An internals location:
internals/, as defined below.
src/
├── index.ts # ✅ entry: export * from "./services"; export * from "./hooks";
├── services/index.ts # ✅ one level below: export * from "./portfolio";
├── services/portfolio/index.ts # ✅ pure chain: entry → services → services/portfolio
├── hooks/index.ts # ✅ one level below, but breaks the chain:
│ # export * from "./useThing"; export * from "../shared";
├── hooks/useThing/index.ts # ❌ nested-barrel: hooks/index.ts re-exports a path outside its folder
├── internals/index.ts # ✅ internals location
└── Row/index.ts # ❌ orphan-barrel: imported by view.ts, re-exported by no entryWhy. A barrel only earns its place as the public face of a package or of one part of it. An
orphan is an indirection no consumer sees, and it outlives its entry: drop "./state" from
exports and state/index.ts stays behind as an orphan, so the same check catches it. Depth alone
hides nothing when each sub-domain has its own face and the folders show the chain. A middle
barrel that filters, renames or adds code does: the name a consumer imports is no longer the name
the folders lead to.
Fix an orphan (orphan-barrel) by asking whether another package uses what it groups:
- Yes: expose it with one
export * from "./dir"line in the root barrel. - No: delete the barrel and import the files directly, or move the folder under
internals/. Expose only code another package imports.
Fix a nested barrel (nested-barrel) at the barrel the violation names, with its line and
reason. In the tree above, export * from "../shared" moves from hooks/index.ts to the entry, or
hooks/index.ts re-exports ./useThing/useThing and the nested barrel goes. An index.tsx that
holds code is a module, not a barrel: rename it after what it declares (Card/index.tsx becomes
Card/Card.tsx).
Purity of what a barrel reaches stays with the rules above. The debt found when the check landed
sits in
intermediate-barrels.js.
It only shrinks: fixing a listed barrel means deleting its entry, and a test fails on an entry
whose file is gone or allowed. Never add one. Packages with skip in exceptions.js are not
checked.
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_EXCEPTIONS entry. 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. Its FILE_EXCEPTIONS lists the reached modules that held violations when
the rule started following export *. That list only shrinks: a test fails on an entry whose file
is gone or clean, so fixing a file means deleting its entry.
Universal components
lint:structure also checks rules R1 to R5 on the React components a dual-platform package
exports, next to the barrel rules above. The X.types.ts contract, the rules and their codes, the
review-only checks and how to clear a waiver are in
universal-components.
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 exposeSee 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.*, or in a file it reaches throughexport *, or in a file named inpackage.json#exports - 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 - a new
index.*belowsrc/that no entry re-exports, or that sits more than one level below an entry through a chain a barrel breaks (see "Where a barrel may sit")
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

