ESM support
Libraries created with create-react-native-library
are pre-configured to work with ESM (ECMAScript Modules) out of the box.
You can verify whether ESM support is enabled by checking the configuration for react-native-builder-bob
in the package.json
file of the library:
"react-native-builder-bob": {
"source": "src",
"output": "lib",
"targets": [
["module", { "esm": true }],
["commonjs", { "esm": true }],
"typescript"
]
}
The "esm": true
option enables ESM-compatible output by adding the .js
extension to the import statements in the generated files. This is necessary if you want to be able to import the library on Node.js or in a bundler that supports ESM, with some caveats. See the Guidelines section for more information.
For TypeScript, it also generates 2 sets of type definitions if the commonjs
target is also enabled: one for the CommonJS build and one for the ES module build.
It's recommended to specify "moduleResolution": "bundler"
and "resolvePackageJsonImports": false
in your tsconfig.json
file to match Metro's behavior (opens in a new tab):
{
"compilerOptions": {
"moduleResolution": "bundler",
"resolvePackageJsonImports": false
}
}
Specifying "moduleResolution": "bundler"
means that you don't need to use file extensions in the import statements. Bob automatically adds them when possible during the build process.
To make use of the output files, ensure that your package.json
file contains the following fields:
"main": "./lib/commonjs/index.js",
"module": "./lib/module/index.js",
"exports": {
".": {
"import": {
"types": "./lib/typescript/module/src/index.d.ts",
"default": "./lib/module/index.js"
},
"require": {
"types": "./lib/typescript/commonjs/src/index.d.ts",
"default": "./lib/commonjs/index.js"
}
},
"./package.json": "./package.json"
},
The main
field is for tools that don't support the exports
field (e.g. Metro (opens in a new tab)). The module
field is a non-standard field that some tools use to determine the ESM entry point.
The exports
field is used by Node.js 12+, modern browsers and tools to determine the correct entry point. The entrypoint is specified in the .
key and will be used when the library is imported or required directly (e.g. import 'my-library'
or require('my-library')
).
Here, we specify 2 conditions:
import
: Used when the library is imported with animport
statement or a dynamicimport()
. It should point to the ESM build.require
: Used when the library is required with arequire
call. It should point to the CommonJS build.
Each condition has 2 fields:
types
: Used for the TypeScript definitions.default
: Used for the actual JS code when the library is imported or required.
You can also specify additional conditions for different scenarios, such as react-native
, browser
, production
, development
etc. Note that support for these conditions depends on the tooling you're using.
The ./package.json
field is used to point to the library's package.json
file. It's necessary for tools that may need to read the package.json
file directly (e.g. React Native Codegen (opens in a new tab)).
Guidelines
There are still a few things to keep in mind if you want your library to be ESM-compatible:
-
Avoid using default exports in your library. Named exports are recommended. Default exports produce a CommonJS module with a
default
property, which will work differently than the ESM build and can cause issues. -
If the library uses platform-specific extensions (e.g.,
.ios.js
or.android.js
), the ESM output will not be compatible with Node.js, i.e. it's not possible to use the library in Node.js withimport
syntax. It's necessary to omit file extensions from the imports to make platform-specific extensions work, however, Node.js requires file extensions to be present.Bundlers such as Metro can handle this without additional configuration. Other bundlers may need to be configured to make extensionless imports to work, (e.g. it's necessary to specify
resolve.fullySpecified: false
(opens in a new tab) for Webpack).It's still possible to use the library in Node.js using the CommonJS build with
require
:const { foo } = require('my-library');
Alternatively, if you want to be able to use the library in Node.js with
import
syntax, you can userequire
to import code with platform-specific extensions in your library:// will import `foo.native.js`, `foo.ios.js`, `foo.js` etc. const { foo } = require('./foo');
Make sure to have a file without any platform-specific extensions that will be loaded by Node.js.
Also note that if your module (e.g.
foo.js
in this case) contains ESM syntax, it will only work on Node.js 20 or newer. -
Avoid using
.cjs
,.mjs
,.cts
or.mts
extensions. Metro always requires file extensions in import statements when using.cjs
or.mjs
which breaks platform-specific extension resolution. -
Avoid using
"moduleResolution": "node16"
or"moduleResolution": "nodenext"
in yourtsconfig.json
file. They require file extensions in import statements which breaks platform-specific extension resolution. -
If you specify a
react-native
condition inexports
, make sure that it comes before thedefault
condition. The conditions should be ordered from the most specific to the least specific:"exports": { ".": { "import": { "types": "./lib/typescript/module/src/index.d.ts", "react-native": "./lib/modules/index.native.js", "default": "./lib/module/index.js" }, "require": { "types": "./lib/typescript/commonjs/src/index.d.ts", "react-native": "./lib/commonjs/index.native.js", "default": "./lib/commonjs/index.js" } }, "./package.json": "./package.json" }