id | title | description |
---|---|---|
adrs-adr004 |
ADR004: Module Export Structure |
Architecture Decision Record (ADR) log on Module Export Structure |
With a growing number of exports of packages like @backstage/core-components
,
it is becoming more and more difficult to answer questions such as
Is the export in this module also exported by the package?
or
What is exported from this directory?
We currently do not use any pattern for how to structure exports. There is a mix
of package-level re-exports deep into the directory tree, shallow re-exports for
each directory, exports using *
and explicit lists of each symbol, etc. The
mix and lack of predictability make it difficult to reason about the boundaries
of a module, and for example knowing whether it is safe to export a symbol in a
given file.
We will make each exported symbol traceable through index files all the way down
to the root of the package, src/index.ts
. Each index file will only re-export
from its own immediate directory children, and only index files will have
re-exports. This gives a file tree similar to this:
index.ts
components/index.ts
/ComponentX/index.ts
/ComponentX.tsx
/SubComponentY.tsx
lib/index.ts
/UtilityX/index.ts
/UtilityX.ts
/helper.ts
To check whether for example SubComponentY
is exported from the package, it
should be possible to traverse the index files towards the root, starting at the
adjacent one. If there is any index file that doesn't export the previous one,
the symbol is not publicly exported. For example, if
components/ComponentX/index.ts
exports SubComponentY
, but
components/index.ts
does not re-export ./ComponentX
, one should be certain
that SubComponentY
is not exported outside the package. This rule would be
broken if for example the root index.ts
re-exports ./components/ComponentX
In addition, index files that are re-exporting other index files should always use wildcard form, that is:
// in components/index.ts
export * from './ComponentX';
Index files that are re-exporting symbols from non-index files should always enumerate all exports, that is:
// in components/ComponentX/index.ts
export { ComponentX } from './ComponentX';
export type { ComponentXProps } from './ComponentX';
Internal cross-directory imports are allowed from non-index modules to index modules, for example:
// in components/ComponentX/ComponentX.tsx
import { UtilityX } from '../../lib/UtilityX';
Imports that bypass an index file are discouraged, but may sometimes be necessary, for example:
// in components/ComponentX/ComponentX.tsx
import { helperFunc } from '../../lib/UtilityX/helper';
We will actively work to rework the export structure in our codebase,
prioritizing the library packages such as @backstage/core-components
and
@backstage/backend-common
.
If possible, we will add tools, such as lint rules, to help enforce the export structure.