Skip to content

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).

json
{
  "name": "some-pkg",
  "exports": {
    ".": "./lib/index.js",
    "./Button": "./lib/components/Button/index.js"
  }
}

Usage:

js
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:

jsonc
{
  "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:

json
{
  "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 (/Button rather than lib/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.

jsonc
{
  "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.

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.