Repo guidance: Package export best practices
WARNING
This page is a work in progress.
Use exports maps
Exports maps (added in Node.js 12 and Webpack 5) are defined under the exports property in package.json and explicitly declare which import paths the package allows. This ensures a minimal API surface, provides intellisense for allowed paths, and supports multiple module formats.
Once a package defines an exports map, Node, Webpack 5+, and most other tools will refuse to resolve arbitrary deep imports of other paths within the package (and this is enforced by lint rules).
{
"name": "some-pkg",
"exports": {
".": "./lib/index.js",
"./Button": "./lib/components/Button/index.js"
}
}Usage:
import { Thing } from 'some-pkg'
import { Button } from 'some-pkg/Button'Multiple entry points
Add subpath exports if a package needs to allow multiple entry points:
{
"exports": {
// Main entry point
".": "./lib/index.js",
// Button only
"./Button": "./lib/components/Button/index.js",
// Alternative entry point for test utilities
"./test": "./lib/test/index.js",
},
}Multiple module formats
To support multiple module formats, use conditions. When a path is resolved using require, the "require" condition is used; when imported in an ES module, "import" is used. This supports both ESM and CJS (and optionally corresponding types) without the path referring to the module flavor:
{
"exports": {
".": {
"require": "./lib-commonjs/index.js",
"import": "./lib-esm/index.js"
},
"./Button": {
"require": "./lib-commonjs/components/Button/index.js",
"import": "./lib-esm/components/Button/index.js"
}
}
}There are many benefits to this:
- No leaky abstractions
- Smaller import paths (
/Buttonrather thanlib/components/Button) - Works with CJS or ESM without special remapping
We can take this a step further by providing a "source" condition for development mode, which helps translate the above mappings into source files (.ts/.tsx) and reduces complexity in resolving modules in a local dev environment. You can also add "types" so TS can resolve types for custom import paths if the source isn't published.
{
"exports": {
".": {
"source": "./src/index.ts",
"types": "./lib/index.d.ts",
// "default" is a generic fallback condition.
// Alternatively (or in addition) you could use "import" or "require"
// as appropriate.
"default": "./lib/index.js",
},
},
}In a Webpack config, the source condition can be added to resolve.conditionNames to auto-resolve source files.
Include ES modules
When publishing a package, include ES module format JS output, ideally instead of CommonJS. Learn more about module formats
At minimum your package.json should include a module property pointing to your ESM, but using exports maps is even better. Note that if your code includes any state or singletons and you need to include multiple module formats, there are some subtleties to setting this up.
Use recommended TypeScript settings
Assuming you use TypeScript to build your project, you should use the isolatedModules setting in your tsconfig.json. This enforces that your code can be transpiled in isolation and prohibits patterns that break that, such as exporting types from other libraries without using export type syntax, or using const enum.
See the recommended settings page for more.
Avoid export *, especially from another package
Re-exporting the entirety of another package (export * from "some-pkg") creates an unpredictable API surface and other problems. It's better to be explicit about exports to prevent accidents like exporting the same name when dependencies conflict.
In some cases, it can be okay to use export * from "./local-path", but this has its own pitfalls, especially in inner index files.
Use local stubs for async imports
When lazily importing from another package, prefer a local stub that re-exports needed symbols. See Use async import stubs.
Exporting images
Cloudpack supports importing images (.jpg | .png | .svg) from within a package or across package boundaries.