owned this note
owned this note
Published
Linked with GitHub
# Why is `export *` bad?
When `export *` is used in libraries, especially when exporting from an external 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
Take for instance package `components` exports * from package `button`. It has a semver dependency on `button` using `^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 *, it's contract implicitly inherits updates from the `button` contract. This violates semver.
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
The esbuild tool does not implement tree shaking through re-exported namespaces:
https://github.com/evanw/esbuild/issues/1420
This particular bug may be an edge case; normally `export * from './file'` re-exports 1 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.
Webpack 5 has an advanced optimization where it can trace usage even through this alias to tree shake out unused references, while esbuild is missing this. 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 `theme` has a method `createTheme`
Package `styling` exports * from `theme`
Package `fui-react` exports * from both `theme` and `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 named modules from the source, rather than indirectly through another library. Example:
`fui-react` exports `createTheme` from `theme`
`styling` exports `createTheme` from `theme`
And,
`fui-react` does NOT export `createTheme` from `styling`
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
https://github.com/evanw/esbuild/issues/1737
If you `export * from 'library'` in a sub file (such as Fluent UI's `/Utilities.js` file), it doesn't retain the `export *` and instead tries to re-emit the namespace through helper code. This makes browser bundles from esbuild unconsumable. Avoiding `export *` from external libraries works around the critical issue.
### api-extractor does not extract `export *` from external libraries
If you `export * from 'library'`, api extractor's output does not include the api surface of the external library, due to it being unpredictable. This means you don't detect an api surface change when it happens, since it can only truely be evaluated on the consumer side.
The maintainers of api-extractor agree that this practice has all the previously mentioned problems and has no appetite to expand the api surface generated by external export * usage.
Because api-extrator detects no changes to an api surface due to external shift, there is less opportunity to enforce good practices elsewhere. For example, Beachball could detect api-extractor shifts and limit bump types, but this requires the extraction actually shifts.
## How to prevent it from being reintroduced
Eslint rule:
Introducing `no-export-all`