webpack’s exports field like environment variables for dynamic import by package users
import Request is issued from ESM syntax or similar. Node.js, webpack, rollup, esinstall(1), wmr(1) require Request is issued from CommonJs/AMD syntax or similar. Node.js, webpack, rollup, esinstall(1), wmr(1) style Request is issued from a stylesheet reference. sass Request is issued from a sass stylesheet reference. asset Request is issued from a asset reference. script Request is issued from a normal script tag without module system. These conditions might also be set additionally:
Condition Description Supported by module All module syntax that allows to reference javascript supports ESM. (only combined with import or require) webpack, rollup, wmr esmodules Always set by supported tools. wmr types Request is issued from typescript that is interested in type declarations. (1) import and require are both set independent of referencing syntax. require has always lower priority.
import The following syntax will set the import condition:
ESM import declarations in ESM JS import() expression HTML
CommonJs require(…) AMD define() AMD require([…]) CommonJs require.resolve() CommonJs (webpack) require.ensure([…]) CommonJs (webpack) require.context CommonJs HMR (webpack) module.hot.accept/decline([…]) HTML
CSS @import HTML asset The following syntax will set the asset condition:
CSS url() ESM new URL(…, import.meta.url) HTML script The following syntax will set the script condition:
HTML
This condition should be used when looking for a javascript file that can be injected as script tag in a HTML page without additional preprocessing.
Optimizations The following conditions are set for various optimizations:
Condition Description Supported by production In a production environment. No devtooling should be included. webpack development In a development environment. Devtooling should be included. webpack Note: Since production and development is not supported by everyone, no assumption should be made when none of these is set.
Target environment The following conditions are set depending on the target environment:
Condition Description Supported by browser Code will run in a browser. webpack, esinstall, wmr electron Code will run in electron.(1) webpack worker Code will run in a (Web)Worker.(1) webpack worklet Code will run in a Worklet.(1) - node Code will run in Node.js. Node.js, webpack, wmr(2) deno Code will run in Deno. - react-native Code will run in react-native. - (1) electron, worker and worklet comes combined with either node or browser, depending on the context.
(2) This is set for browser target environment.
Since there are multiple versions of each environment the following guidelines apply:
node: See engines field for compatibility. browser: Compatible with current Spec and stage 4 proposals at time of publishing the package. Polyfilling resp. transpiling must be handled on consumer side. Features that are not possible to polyfill or transpile should be used carefully as it limits the possible usage. deno: TBD react-native: TBD Conditions: Preprocessor and runtimes The following conditions are set depending on which tool preprocesses the source code.
Condition Description Supported by webpack Processed by webpack. webpack Sadly there is no node-js condition for Node.js as runtime. This would simplify creating exceptions for Node.js.
Conditions: Custom The following tools support custom conditions:
Tool Supported Notes Node.js yes Use —conditions CLI argument. webpack yes Use resolve.conditionNames configuration option. rollup yes Use exportConditions option for @rollup/plugin-node-resolve esinstall no wmr no For custom conditions the following naming schema is recommended:
Examples: example-corp:beta, google:internal.
Common patterns All patterns are explained with a single ”.” entry into the package, but they can be extended from multiple entries too, by repeating the pattern for each entry.
These pattern should be used as guide not as strict ruleset. They can be adapted to the individual packages.
These pattern are based on the following list of goals/assumptions:
Packages are rotting. We assume at some point packages are no longer being maintained, but they are continued to be used. exports should be written to use fallbacks for unknown future cases. default condition can be used for that. As the future is unknown we assume an environment similar to browsers and module system similar to ESM. Not all conditions are supported by every tool. Fallbacks should be used to handled these cases. We assume the following fallback make sense in general: ESM > CommonJs Production > Development Browser > node.js Depending on the package intention maybe something else makes sense and in this case the patterns should be adopted to that. Example: For a command line tool a browser-like future and fallback doesn’t make a lot of sense, and in this case node.js-like environments and fallbacks should be used instead.
For complex use cases multiple patterns need to be combined by nesting these conditions.
Target environment independent packages These patterns make sense for packages that do not use environment specific APIs.
Providing only an ESM version { “type”: “module”, “exports”: “./index.js” } Note: Providing only a ESM comes with restrictions for node.js. Such a package would only work in Node.js >= 14 and only when using import. It won’t work with require().
Providing CommonJs and ESM version (stateless) { “type”: “module”, “exports”: { “node”: { “module”: “./index.js”, “require”: “./index.cjs” }, “default”: “./index.js” } } Most tools get the ESM version. Node.js is an exception here. It gets a CommonJs version when using require(). This will lead to two instances of these package when referencing it with require() and import, but that doesn’t hurt as the package doesn’t have state.
The module condition is used as optimization when preprocessing node-targeted code with a tool that supports ESM for require() (like a bundler, when bundling for Node.js). For such a tool the exception is skipped. This is technically optional, but bundlers would include the package source code twice otherwise.
You can also use the stateless pattern if you are able to isolate your package state in JSON files. JSON is consumable from CommonJs and ESM without polluting the graph with the other module system.
Note that here stateless also means class instances are not tested with instanceof as there can be two different classes because of the double module instantiation.
Providing CommonJs and ESM version (stateful) { “type”: “module”, “exports”: { “node”: { “module”: “./index.js”, “import”: “./wrapper.js”, “require”: “./index.cjs” }, “default”: “./index.js” } } // wrapper.js import cjs from ‘./index.cjs’;
export const A = cjs.A; export const B = cjs.B; In a stateful package we must ensure that the package is never instantiated twice.
This isn’t a problem for most tools, but Node.js is again an exception here. For Node.js we always use the CommonJs version and expose named exports in the ESM with a ESM wrapper.
We use the module condition as optimization again.
Providing only a CommonJs version { “type”: “commonjs”, “exports”: “./index.js” } Providing “type”: “commonjs” helps to statically detect CommonJs files.
Providing a bundled script version for direct browser consumption { “type”: “module”, “exports”: { “script”: “./dist-bundle.js”, “default”: “./index.js” } } Note that despite using “type”: “module” and .js for dist-bundle.js this file is not in ESM format. It should use globals to allow direct consumption as script tag.
Providing devtools or production optimizations These patterns make sense when a package contains two versions, one for development and one for production. E. g. the development version could include additional code for better error message or additional warnings.
Without Node.js runtime detection { “type”: “module”, “exports”: { “development”: “./index-with-devtools.js”, “default”: “./index-optimized.js” } } When the development condition is supported we use the version enhanced for development. Otherwise, in production or when mode is unknown, we use the optimized version.
With Node.js runtime detection { “type”: “module”, “exports”: { “development”: “./index-with-devtools.js”, “production”: “./index-optimized.js”, “node”: “./wrapper-process-env.cjs”, “default”: “./index-optimized.js” } } // wrapper-process-env.cjs if (process.env.NODE_ENV !== ‘development’) { module.exports = require(‘./index-optimized.cjs’); } else { module.exports = require(‘./index-with-devtools.cjs’); } We prefer static detection of production/development mode via the production or development condition.
Node.js allows to detection production/development mode at runtime via process.env.NODE_ENV, so we use that as fallback in Node.js. Sync conditional importing ESM is not possible and we don’t want to load the package twice, so we have to use CommonJs for the runtime detection.
When it’s not possible to detect mode we fallback to the production version.
Providing different versions depending on target environment A fallback environment should be chosen that makes sense for the package to support future environments. In general a browser-like environment should be assumed.
Providing Node.js, WebWorker and browser versions { “type”: “module”, “exports”: { “node”: “./index-node.js”, “worker”: “./index-worker.js”, “default”: “./index.js” } } Providing Node.js, browser and electron versions { “type”: “module”, “exports”: { “electron”: { “node”: “./index-electron-node.js”, “default”: “./index-electron.js” }, “node”: “./index-node.js”, “default”: “./index.js” } } Combining patterns Example 1 This is an example for a package that has optimizations for production and development usage with runtime detection for process.env and also ships a CommonJs and ESM version
{ “type”: “module”, “exports”: { “node”: { “development”: { “module”: “./index-with-devtools.js”, “import”: “./wrapper-with-devtools.js”, “require”: “./index-with-devtools.cjs” }, “production”: { “module”: “./index-optimized.js”, “import”: “./wrapper-optimized.js”, “require”: “./index-optimized.cjs” }, “default”: “./wrapper-process-env.cjs” }, “development”: “./index-with-devtools.js”, “production”: “./index-optimized.js”, “default”: “./index-optimized.js” } } Example 2 This is an example for a package that supports Node.js, browser and electron, has optimizations for production and development usage with runtime detection for process.env and also ships a CommonJs and ESM version.
{ “type”: “module”, “exports”: { “electron”: { “node”: { “development”: { “module”: “./index-electron-node-with-devtools.js”, “import”: “./wrapper-electron-node-with-devtools.js”, “require”: “./index-electron-node-with-devtools.cjs” }, “production”: { “module”: “./index-electron-node-optimized.js”, “import”: “./wrapper-electron-node-optimized.js”, “require”: “./index-electron-node-optimized.cjs” }, “default”: “./wrapper-electron-node-process-env.cjs” }, “development”: “./index-electron-with-devtools.js”, “production”: “./index-electron-optimized.js”, “default”: “./index-electron-optimized.js” }, “node”: { “development”: { “module”: “./index-node-with-devtools.js”, “import”: “./wrapper-node-with-devtools.js”, “require”: “./index-node-with-devtools.cjs” }, “production”: { “module”: “./index-node-optimized.js”, “import”: “./wrapper-node-optimized.js”, “require”: “./index-node-optimized.cjs” }, “default”: “./wrapper-node-process-env.cjs” }, “development”: “./index-with-devtools.js”, “production”: “./index-optimized.js”, “default”: “./index-optimized.js” } } Looks complex, yes. We were already able to reduce some complexity due to a assumption we can make: Only node need a CommonJs version and can detect production/development with process.env.