Repo guidance: Avoid importing from inner-package index files
index.ts files (sometimes referred to as "barrel" files) re-export a public API surface from various source files within a package. These can be a good, predictable way to identify what the package does and what can be used. However, they can also be misused.
General rules for index files
First, some general rules to follow regarding index.ts files:
- The main
index.tsfile (oftensrc/index.ts) represents an entry point into your package. Your package.jsonexportsmap should refer to its corresponding output file as the top-level export,.: e.g.".": "./lib/index.js". - If you have more than one
index.ts/entry point file, your package might be too big. Consider separating it into smaller packages; this allows for smaller versioned units to be consumed with more manageable contracts.- There are cases where multiple entry points are necessary or desirable. For example, test/mock code should not be exported from the main
index.ts, but from a separate file. The exports map can also specify other sub-path imports if necessary.
- There are cases where multiple entry points are necessary or desirable. For example, test/mock code should not be exported from the main
How index files can be misused
The most common case of index file misuse involves importing things defined within the same package from anywhere other than the original source file.
Imagine you have this file structure:
/src/
index.ts - exports * from file1 and file2
file1.ts - imports a bunch of massive dependencies, exports func1
file2.ts - exports func2
file3.ts - exports func3, depends on func2(See also general issues with export *.)
file3.ts needs func2; it can import it using one of the following lines:
// good
import { func2 } from './file2'
// bad
import { func2 } from './index'Why is importing from an index file within the same package bad?
- They inherit implications about module ordering
- If
index.tsexports fromfile1and thenfile2, there is an implied module order. The exported files also carry implications: file-scoped code can run in a particular order, modules are defined in a particular order, etc. Bundlers do their best to preserve all these implications and to understand your intent, but sometimes bad things happen. Webpack has "module concatenation bailout" which can cause code to be included rather than tree shaken. esbuild has its own bugs that output modules in the wrong order. - All these implications can be completely avoided if we reduce the imported file to the file containing the source.
- If
- You create risk of extra build-time work for the parser to resolve your module
- We had to parse
file1.tsfirst, which is expensive due to its large dependencies - Even if it's not expensive today, things change tomorrow
- We had to parse
- You can create risk of extra runtime work to resolve your module
- If only one bundle file is emitted, this is not a concern. As soon as we have async split points and common chunks, ambiguity in the import can lead to extra code being parsed to find the imported value.
- It can create cycles between files within the same package, resulting in load order-related bugs
- Even though you just want
func2, using the index file inherits its load order requirements. Some of those load orders may conflict with your expectations, especially when things exported from index depend on your code!
- Even though you just want
- Bundler bugs can kick in and give you a bad day.
- You're being roundabout in what you want: do you want index to be parsed and resolved first, or do you just want the module with the least amount of implications? Sometimes this vagueness can trigger problematic edge cases in the tools.
Enforcement
No lint rule is available for enforcement today, but this is planned for a future rule in @ms-cloudpack/eslint-plugin.