changed 2 years ago
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

Select a repo