Package Exports Support (Experimental)
Backgroundβ
Introduced in Node.js 12.7.0, Package Exports is a modern approach for npm packages to specify entry points β the mapping of package subpaths which can be externally imported and which file(s) they should resolve to.
When Package Exports support is enabled via resolver.unstable_enablePackageExports
, Metro's module resolution algorithm will consider the "exports"
field in package.json
files.
- Node.js spec
- RFC for Package Exports in Metro
- React Native announcement post (coming soon!)
Configuration optionsβ
Option | Description |
---|---|
resolver.unstable_enablePackageExports | Enable Package Exports support. |
resolver.unstable_conditionNames | The set of condition names to assert when resolving conditional exports. |
resolver.unstable_conditionsByPlatform | The additional condition names to assert when resolving for a given platform target. |
Summary of breaking changesβ
Package Exports resolution is available since Metro 0.76.1 and is disabled by default. We will provide the option to disable it for a long time yet, and have no plans to remove existing non-"exports"
resolution behaviour.
Since Package Exports features overlap with existing React Native concepts (such as platform-specific extensions), and since "exports"
had been live in the npm ecosystem for some time, we reached out to the React Native community to make sure our implementation would meet developers' needs (PR, final RFC).
This led us to create an implementation of Package Exports in Metro that is spec-compliant (necessitating some breaking changes), but backwards compatible otherwise (helping apps with existing imports to migrate gradually).
Breaking: Match "exports"
first, then fall back to legacy resolutionβ
If present in a package.json
file, "exports"
will be the first field consulted when resolving a package.
"exports"
will be used instead of any existing"react-native"
,"browser"
, or"main"
field βΒ or a file on disk at the same subpath (edge case).- Fallback: If the requested subpath is not matched in
"exports"
, Metro will try to resolve it again, considering the above fields.
Subpaths matched in "exports"
(including via subpath patterns) will use the exact target file path specified by a package.
- Metro will not expand
sourceExts
against the import specifier. - Metro will not resolve platform-specific extensions against the target file.
- Unchanged: Metro will expand asset densities (e.g.
icon.png
βicon@2x.png
) if the target file is an asset.
Exampleβ
For a package without an "exports"
field, Metro tries multiple potential file locations based on the import specifier:
import FooComponent from 'some-pkg/FooComponent';
// Tries .[platform].js, .native.js, .js (+ TypeScript variants)
However, if "./FooComponent"
is listed in "exports"
, Metro matches the import specifier to this subpath, and uses the target file specified by the package with no further rules:
import FooComponent from 'some-pkg/FooComponent';
// Resolves exact target from "exports" only
We have no plans to drop platform-specific extensions for packages not using "exports"
, or in app code.
Breaking: Import specifiers are matched exactlyβ
Previously, import specifiers (the string given to import
or require()
) could be defined using both extensioned or extensionless paths. This is no longer the case for subpath keys in the "exports"
field.
Exampleβ
{
"name": "some-pkg",
"exports": {
"./FooComponent": "./src/FooComponent.js"
}
}
import FooComponent from 'some-pkg/FooComponent.js';
// Inaccessible unless the package had also listed "./FooComponent.js"
// as an "exports" key
Note that this behaviour also applies for subpath patterns: "./*": "./src/*.js"
is distinct from "./*.js": "./src/*.js"
.
Package encapsulation is lenientβ
In Node.js, it is an error to import package subpaths that aren't explicitly listed in "exports"
. In Metro, we've decided to handle these errors leniently and resolve modules following the old behavior as necessary. This is intended to reduce user friction for previously allowed imports in existing Metro projects.
Instead of throwing an error, Metro will log a warning and fall back to file-based resolution.
warn: You have imported the module "foo/private/fn.js" which is not listed in
the "exports" of "foo". Consider updating your call site or asking the package
maintainer(s) to expose this API.
We plan to implement a strict mode for package encapsulation in future, to align with Node's default behavior. We recommend that all developers fix encapsulation warnings in their code.
Migration guide for package maintainersβ
Adding an "exports"
field to your package is entirely optional. Existing package resolution features will behave identically for packages which don't use "exports"
β and we have no plans to remove this behaviour.
Recommended: Introducing "exports"
is a breaking changeβ
The Node.js spec gives guidance on migrating to "exports"
in a non-breaking manner, however this is challenging in practice. For instance, if your React Native package uses platform-specific extensions on its public exports, this is a breaking change by default.
To make the introduction of
"exports"
non-breaking, ensure that every previously supported entry point is exported. It is best to explicitly specify entry points so that the package's public API is well-defined.βΒ https://nodejs.org/docs/latest-v19.x/api/packages.html#package-entry-points
Package subpathsβ
Please do not rely on lenient package encapsulation under Metro. While Metro does this for backwards compatibility, packages should follow how "exports"
is documented in the spec and strictly implemented by other tools.
File extensions are important!β
Each subpath is an exact specifier (see section in RFC).
We recommend continuing to use extensionless specifiers for subpaths in packages targeting React Native βΒ or defining both extensioned and extensionless specifiers. This will match matching existing user expectations.
"exports": {
".": "./src/index.js",
"./FooComponent": "./src/FooComponent.js",
"./FooComponent.js": "./src/FooComponent.js"
}
Subpath patterns do not permit expansionβ
Subpath patterns are a shorthand for mapping multiple subpaths βΒ they do not permit path expansion (strictly a substring replacement), however will match nested directories (see section in RFC).
Only one *
is permitted per side of a subpath pattern.
"exports": {
".": "./index.js",
"./utils/*": "./utils/*.js"
}
'pkg/utils/foo'
matches'pkg/utils/foo.js'
.'pkg/utils/foo/bar'
matches'pkg/utils/foo/bar.js'
.'pkg/utils/foo'
does not match'pkg/utils/foo.bar.js'
.
Replacing "browser"
and "react-native"
fieldsβ
We've introduced "react-native"
as a community condition (for use with conditional exports). This represents React Native, the framework, sitting alongside other recognised runtimes such as "node"
and "deno"
(RFC).
Community Conditions Definitions βΒ
"react-native"
Will be matched by the React Native framework (all platforms). To target React Native for Web, "browser" should be specified before this condition.
This replaces the previous "react-native"
root field. The priority order for how this was previously resolved was determined by projects, which created ambiguity when using React Native for Web. Under "exports"
, packages concretely define the resolution order for conditional entry points βΒ removing this ambiguity.
Example: Use conditional exports to target web and React Nativeβ
"exports": {
"browser": "./dist/index-browser.js",
"react-native": "./dist/index-react-native.js",
"default": "./dist/index.js"
}
We chose not to introduce "android"
and "ios"
conditions, due to the prevalence of other existing platform selection methods, and the complexity of how this behavior might work across frameworks. We recommend the Platform.select()
API instead.
Replacing platform-specific extensionsβ
Breaking change: Subpaths matched in
"exports"
(including via subpath patterns) will use the exact file path specified by a package, and will not attempt to expandsourceExts
or platform-specific extensions.
Use Platform.select()
(React Native)β
"exports": {
"./FooComponent": "./src/FooComponent.js"
}
// src/FooComponent.js
const FooComponent = Platform.select({
android: require('./FooComponentAndroid.js'),
ios: require('FooComponentIOS.js'),
});
export default FooComponent;
Asset filesβ
As with source files, assets must be listed in "exports"
to be imported without warnings. Asset files with multiple densities, e.g. icon.png
and icon@2x.png
, will continue to work without being listed individually.
Using subpath patterns can be a convenient method to export many assets. We recommend specifying asset subpaths with their file extension.
{
"exports": {
"./assets/*.png": "./dist/assets/*.png"
}
}