website/blog/2023-06-21-package-exports-support.md
With the release of React Native 0.72, Metro — our JavaScript build tool — now includes beta support for the package.json "exports" field. When enabled, it adds the following functionality:
In this post we'll cover how Package Exports works, and what these changes mean for you as a React Native app developer or package maintainer.
<!-- truncate -->Introduced in Node.js 12.7.0, Package Exports is the 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.
Supporting "exports" improves how React Native projects will work with the wider JavaScript ecosystem (used in ~16.6k packages today), and gives package authors a standardised feature set for multiplatform packages to target React Native.
"exports" can be used alongside, or instead of, "main" in a package.json file.
{
"name": "@storybook/addon-actions",
"main": "./dist/index.js",
...
"exports": {
".": {
"node": "./dist/index.js",
"import": "./dist/index.mjs",
"default": "./dist/index.js"
},
"./preview": {
"import": "./dist/preview.mjs",
"default": "./dist/preview.js"
},
...
"./package.json": "./package.json"
}
}
Here's some app code consuming the above package by importing different subpaths of @storybook/addon-actions.
import {action} from '@storybook/addon-actions';
// -> '@storybook/addon-actions/dist/index.js'
import {action} from '@storybook/addon-actions/preview';
// -> '@storybook/addon-actions/dist/preview.js'
import helpers from '@storybook/addon-actions/src/preset/addArgsHelpers';
// Inaccessible - not listed in "exports"!
The headlining features of Package Exports are:
"exports" can be imported from outside the package — giving packages control over their public API."node", "browser", or "react-native" runtimes — replacing the "browser" field spec.:::note
The full capabilities for "exports" are detailed in the Node.js Package Entry Points spec.
Since these 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).
:::
Package Exports can be enabled today, in beta.
"browser" conditional export, removing the need for workarounds.Enabling Package Exports brings a few edge-case breaking changes that may affect specific projects, and which you can test today.
In a future React Native release, Package Exports will be enabled by default. In a chicken-and-egg situation, React Native apps were previously a holdout for some packages to migrate to "exports" — or used our "react-native" root field escape hatch. Supporting these features in Metro will allow the ecosystem to move forward.
Package Exports can be enabled in your app's metro.config.js file via the resolver.unstable_enablePackageExports option.
const config = {
// ...
resolver: {
unstable_enablePackageExports: true,
},
};
Metro exposes two further resolver options which configure how conditional exports behave:
unstable_conditionNames — The set of condition names to assert when resolving conditional exports. By default, we match ['require', 'import', 'react-native'].unstable_conditionsByPlatform — The additional condition names to assert when resolving for a given platform target. By default, this matches 'browser' when the platform is 'web'.:::tip
Remember to use the React Native Jest preset! Jest includes support for Package Exports by default. In tests, you can override which customExportConditions are resolved using the testEnvironmentOptions option.
If you are using TypeScript, resolution behaviour can be matched by setting moduleResolution: 'bundler' and resolvePackageJsonImports: false within your project's tsconfig.json.
:::
For existing projects, we recommend that early adopters follow these steps to see if resolution changes occur after enabling unstable_enablePackageExports. This is a one-time process. It's likely that there will be no changes at all, but we'd like developers to opt in with certainty.
:::note
If you are not using Yarn, substitute yarn for npx (or the relevant tool used in your project).
:::
Get all resolved dependencies (before changes):
# Replace index.js with your entry file if needed, such as App.js
yarn metro get-dependencies index.js --platform android --output before.txt
npx expo customize metro.config.js if your project doesn't have a metro.config.js file yet.--platform android for the other platforms in use by your app (e.g. ios, web).Enable resolver.unstable_enablePackageExports in metro.config.js.
Get all resolved dependencies (after changes):
yarn metro get-dependencies index.js --platform android --output after.txt
Compare!
diff before.txt after.txt
We decided on 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).
The key breaking change is that when "exports" is provided by a package, it will be consulted first (before any other package.json fields) — and a matched subpath target will be used directly.
sourceExts against the import specifier.For more details, please see all breaking changes in the Metro docs.
When Metro encounters a subpath that isn't listed in "exports", it will fall back to legacy resolution. This is a compatibility feature intended to reduce user friction for previously allowable imports in existing React Native projects.
Instead of throwing an error, Metro will log a warning.
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.
:::note We plan to implement a strict mode for package encapsulation in future, to align with Node's default behaviour. Therefore, we recommend that all developers address these warnings if raised by users. :::
:::info Per our rollout plan, Package Exports will be enabled for most projects in the next React Native release (0.73) later this year.
We have no plans to remove support for the "main" field and other current package resolution features any time soon.
:::
Package Exports provides the ability to restrict access to your package's internals, and more predictable capabilities for libraries to target React Native and React Native for Web.
"exports" todayIf your package uses "exports" alongside the current "react-native" root field, please bear in mind the breaking changes for users above. For users enabling this feature in Metro, "exports" will now be considered first during module resolution.
In practice, we anticipate the main change for users will be the enforcement (via warnings) of any inaccessible subpaths in their apps, from respecting "exports" package encapsulation.
"exports"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.
We believe that the new features of "exports" provide a compelling feature set for React Native package maintainers.
"react-native" and "browser"), we now give packages control of the resolution order of these conditions (see next heading).If you decide to introduce "exports", we recommend making this as a breaking change. We've prepared a migration guide in the Metro docs which includes how to replace features such as platform-specific extensions.
:::note
Please do not rely on the lenient behaviours of Metro's implementation. While Metro is backwards-compatible, packages should follow how "exports" is documented in the spec and strictly implemented by other tools.
:::
"react-native" conditionWe'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).
:::info
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.
"exports": {
"browser": "./dist/index-browser.js",
"react-native": "./dist/index-react-native.js",
"default": "./dist/index.js"
}
:::note
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 behaviour might work across frameworks. Please use the Platform.select() API instead.
:::
"exports", enabled by defaultIn the next React Native release, we are aiming to remove the unstable_ prefix for this feature (having addressed planned performance work and any bugs) and will enable Package Exports resolution by default.
With "exports" enabled for everyone, we can begin taking the React Native community forward — for example, React Native's core packages could be updated to better separate public and internal modules.
Thanks to members of the React Native community that gave feedback on the RFC: @SimenB, @tido64, @byCedric, @thymikee.
Huge thanks to @motiz88 and @robhogan at Meta for supporting the development of this feature.