Conditional Package Exports

Conditional package exports let a JavaScript package map the same public subpath to different files depending on how the package is being resolved, such as `import` versus `require` or browser-oriented versus Node-oriented environments. They are configured through the package's `exports` field in `package.json`.

The `exports` field defines the package's public entry points and can associate each entry point with conditions. A resolver evaluates those conditions and selects the first matching target according to the relevant resolution rules. This allows one package specifier such as `pkg` or `pkg/feature` to resolve to different files without requiring consumers to know the package's internal layout.

Common conditions include `import` and `require`, which distinguish ESM and CommonJS resolution, and environment-oriented conditions such as `browser` or `node` when supported by the resolver. Conditions can be nested, allowing a package to express combinations such as environment first and module system second.

The order of condition keys matters when multiple conditions could match. More specific conditions are normally placed before broader fallbacks such as `default`, and package authors should design the condition tree so that supported resolvers reach an intentional target rather than accidentally selecting an incompatible file.

Adding `exports` also changes package encapsulation: paths that are not explicitly exported can become inaccessible through package subpath imports, even if the corresponding files physically exist. This makes `exports` both a resolution mechanism and a public API boundary.

For frontend engineers, conditional exports are especially useful when a package needs separate builds for browsers and server environments, or when it publishes both ESM and CommonJS entry points. They should be tested with the actual bundlers, runtimes, and package managers the package intends to support because condition support and resolution behavior can differ.

Key points

Interview questions

What problem does conditional package exports solve?

They let a package expose different implementation files for different consumer contexts. For example, a package can provide an ESM target for `import` and a CommonJS target for `require`. Environment conditions can also select browser- or Node-specific implementations when supported by the resolver. This keeps consumers on the same package specifier while allowing the package to publish multiple builds.

How does the `exports` field differ from `main`?

`main` provides a traditional package entry point, whereas `exports` provides a more explicit package interface. `exports` can define multiple public subpaths and attach conditions to their targets. It can also prevent consumers from importing package files that are not exported. For modern packages, this makes `exports` useful for controlling both resolution and API surface.

Why does the order of conditional exports matter?

A resolver can encounter more than one condition that applies to the current context, so the ordering of condition branches can determine which target wins. A package should put the intended specific cases before a broad fallback such as `default`. Incorrect ordering can make a resolver select a target that is valid syntactically but inappropriate for the consumer. This is particularly important when conditions are nested or when multiple tools use different supported condition sets.

How would you publish both ESM and CommonJS builds with conditional exports?

I would expose the package entry point through an `exports` condition map with separate `import` and `require` targets. The ESM target would point to the ESM build and the CommonJS target to the CommonJS build. I would also provide an appropriate fallback when needed and verify that both builds expose compatible APIs. Finally, I would test resolution in the Node versions and bundlers that the package supports.

What risks arise when adding `exports` to an existing package?

Adding `exports` can make previously reachable internal paths unavailable because only declared package paths remain public. Existing applications may therefore fail if they rely on undocumented deep imports. It can also expose differences between tools if their condition handling is not identical. A safe migration starts by identifying existing consumer entry points, explicitly exporting supported paths, and testing representative runtimes and bundlers.