Repo guidance: Avoid export *
When export * is used in libraries, especially when exporting from another library, numerous issues can occur. It is almost always better to export specific named modules.
Common problems it can introduce
API surface implicitly inherits dependency surfaces
For example, suppose package components does export * from 'button'. It has a dependency on button version ^1.0.0.
The button package adds a minor feature. This bumps the package to ^1.1.0.
Despite this following semver and being valid, when the user updates their dependencies, the components package does not minor bump, because it was not changed. However, due to the export *, its contract implicitly inherits updates from the button contract. This violates semver.
It's also possible that the effective API of components could vary depending on the currently resolved version of button in the repo (or even the installation layout if there are multiple versions), creating further unpredictability.
This becomes far worse if the dependency is even looser; though it's unlikely, if components required >1.0.0, major changes to button can create implicit major shifts on the components api surface.
This all becomes far more obvious if components exports named things from button. While it may not catch non-name changes like argument changes, it will enforce explicit named resolutions and catch things like dropped methods or renames.
Breaks tree shaking opportunities
esbuild does not implement tree shaking through re-exported namespaces (tracking issue), e.g. export * as foo from './file'. (Webpack 5 has an advanced optimization where it can trace usage even through this alias to tree shake out unused references.)
This particular bug may be an edge case specific to namespace exports. Normally export * from './file' (no namespace) re-exports one or more individual names. When export * as foo from './file' is used, the alias acts as a single named export, similar to exporting an enum or const object definition.
Avoiding export * aliasing specifically would work around the limitation, or just never using export * would avoid the subtlety completely.
Prevents loading packages in isolation
Consider this:
- Package
themehas a methodcreateTheme - Package
stylingdoesexport * from 'theme' - Package
fui-reactdoes bothexport * from 'theme'andexport * from 'styling'
The user imports createTheme from fui-react. Does it resolve to the createTheme exported by the theme package, or the createTheme exported from the styling package?
If these two exports resolve to the same module, this becomes non-ambiguous. But this means that module resolution can break the export, making it fragile and up to the resolution logic. If for example, the styling package requires a different version of createTheme than the theme package does, they can resolve to different implementations.
It is far less error-prone to avoid this issue altogether and avoid export *. Further, it's better to export identifiers from the source, rather than indirectly through another library. Example:
stylingexportscreateThemefromthemefui-reactexportscreateThemedirectly fromthemefui-reactdoes NOT exportcreateThemefromstyling
By using named module resolution, we are forced to be explicit about the source of truth, resolving ambiguity. This also has performance benefits: now tools like bundlers must only traverse theme when importing createTheme.
esbuild does not retain export * in sub-folder path files
If you export * from 'library' in a sub-file (such as @fluentui/react/lib/Utilities.js), it doesn't retain the export * and instead tries to re-emit the namespace through helper code. This makes browser bundles from esbuild invalid. Avoiding export * from other libraries works around the critical issue.
webpack/rspack do not retain export * in entry point files
If you export * from 'library' in an entry point (such as Fluent UI's lib/index.js file), it completely drops the export *. This makes browser bundles from webpack/rspack have missing exports. Avoiding export * from other libraries works around the critical issue.
API Extractor does not expand export * from other libraries
If you export * from 'library', API Extractor's output does not include the API surface of the other library, since it can vary depending on the resolved version in the consumer's repo. This means it can't help you detect library API surface changes, since the effective API can only truly be evaluated on the consumer side.
Because API Extractor detects no changes in this case, there is less opportunity to enforce good practices elsewhere. For example, Beachball could detect API Extractor shifts and limit bump types (not currently implemented), but this requires that the extracted API actually shifts.
Enforcement
The recommended config from @ms-cloudpack/eslint-plugin bans export * using the @rnx-kit/no-export-all rule.
This rule provides automatic fixes by traversing all of the identifiers exported from a given path, and replacing * with the complete list. The limitation of this approach is that it only lists the exported identifiers when the fixer is initially run; new identifiers must be added manually.