Skip to content

Commit f11b666

Browse files
authored
refactor: drop commonjs build from default setup (#800)
There's a possibility of [dual package hazard](https://nodejs.org/docs/latest-v19.x/api/packages.html#dual-package-hazard) with the current setup. Metro enables `exports` support from [0.82.0](https://github.com/facebook/metro/releases/tag/v0.82.0), so this can become a problem for some packages. So this drops the dual package setup in favor of an ESM-only setup. To make a similar change in your package, apply the following change to your entrypoints: ```diff - "main": "./lib/commonjs/index.js", + "main": "./lib/module/index.js",a - "types": "./lib/typescript/commonjs/src/index.d.ts", "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" - } + "types": "./lib/typescript/src/index.d.ts", + "default": "./lib/module/index.js" } }, ``` Also, remove the `commonjs` target from the `react-native-builder-bob` field in your `package.json` or `bob.config.js` or `bob.config.mjs`: ```diff "react-native-builder-bob": { "source": "src", "output": "lib", "targets": [ ["module", { "esm": true }], - ["commonjs", { "esm": true }] "typescript", ] } ``` With this change, [Jest](https://jestjs.io/) will break for the consumers of your libraries due to the usage of ESM syntax. So they may need to update their Jest configuration to transform your library: ```js module.exports = { preset: 'react-native', + transform: { + 'node_modules/(your-library|another-library)': 'babel-jest', + }, }; ``` If consumers of your library are using it in NodeJS in a CommonJS environment, they'll need to use at least NodeJS v20.19.0 to be able to synchronously `require` your library. You can read more at our [ESM support docs](https://callstack.github.io/react-native-builder-bob/esm#dual-package-setup).
1 parent a8fd434 commit f11b666

File tree

6 files changed

+106
-75
lines changed

6 files changed

+106
-75
lines changed

docs/pages/build.md

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -78,18 +78,11 @@ To configure your project manually, follow these steps:
7878

7979
```json
8080
"source": "./src/index.tsx",
81-
"main": "./lib/commonjs/index.js",
82-
"module": "./lib/module/index.js",
81+
"main": "./lib/module/index.js",
8382
"exports": {
8483
".": {
85-
"import": {
86-
"types": "./lib/typescript/module/src/index.d.ts",
87-
"default": "./lib/module/index.js"
88-
},
89-
"require": {
90-
"types": "./lib/typescript/commonjs/src/index.d.ts",
91-
"default": "./lib/commonjs/index.js"
92-
}
84+
"types": "./lib/typescript/src/index.d.ts",
85+
"default": "./lib/module/index.js"
9386
},
9487
"./package.json": "./package.json"
9588
},
@@ -248,7 +241,7 @@ Example:
248241

249242
Enable compiling source files with Babel and use CommonJS module system. This is essentially the same as the `module` target and accepts the same options, but transforms the `import`/`export` statements in your code to `require`/`module.exports`.
250243

251-
This is useful for supporting usage of this module with `require` in Node versions older than 20 (it can still be used with `import` for Node.js 12+ if `module` target with `esm` is enabled), and some tools such a [Jest](https://jestjs.io). The output file should be referenced in the `main` field. If you have a dual module setup with both ESM and CommonJS builds, it needs to be specified in `exports['.'].require` field of `package.json`.
244+
This is useful for supporting usage of this module with `require` in Node versions older than 20 (it can still be used with `import` for Node.js 12+ if `module` target with `esm` is enabled), and some tools such as [Jest](https://jestjs.io). The output file should be referenced in the `main` field. If you have a [dual package setup](esm.md#dual-package-setup) with both ESM and CommonJS builds, it needs to be specified in `exports['.'].require` field of `package.json`.
252245

253246
Example:
254247

docs/pages/esm.md

Lines changed: 87 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -35,34 +35,22 @@ Specifying `"moduleResolution": "bundler"` means that you don't need to use file
3535
To make use of the output files, ensure that your `package.json` file contains the following fields:
3636

3737
```json
38-
"main": "./lib/commonjs/index.js",
39-
"module": "./lib/module/index.js",
38+
"main": "./lib/module/index.js",
4039
"exports": {
4140
".": {
42-
"import": {
43-
"types": "./lib/typescript/module/src/index.d.ts",
44-
"default": "./lib/module/index.js"
45-
},
46-
"require": {
47-
"types": "./lib/typescript/commonjs/src/index.d.ts",
48-
"default": "./lib/commonjs/index.js"
49-
}
41+
"types": "./lib/typescript/src/index.d.ts",
42+
"default": "./lib/module/index.js"
5043
},
5144
"./package.json": "./package.json"
5245
},
5346
```
5447

55-
The `main` field is for tools that don't support the `exports` field (e.g. [Jest](https://jestjs.io)). The `module` field is a non-standard field that some tools use to determine the ESM entry point.
48+
The `main` field is for tools that don't support the `exports` field (e.g. [Metro](https://metrobundler.dev) < 0.82.0).
5649

5750
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')`).
5851

5952
Here, we specify 2 conditions:
6053

61-
- `import`: Used when the library is imported with an `import` statement or a dynamic `import()`. It should point to the ESM build.
62-
- `require`: Used when the library is required with a `require` call. It should point to the CommonJS build.
63-
64-
Each condition has 2 fields:
65-
6654
- `types`: Used for the TypeScript definitions.
6755
- `default`: Used for the actual JS code when the library is imported or required.
6856

@@ -72,6 +60,76 @@ The `./package.json` field is used to point to the library's `package.json` file
7260

7361
> Note: Metro enables support for `package.json` exports by default from version [0.82.0](https://github.com/facebook/metro/releases/tag/v0.82.0). In previous versions, experimental support can be enabled by setting the `unstable_enablePackageExports` option to `true` in the [Metro configuration](https://metrobundler.dev/docs/configuration/). If this is not enabled, Metro will use the entrypoint specified in the `main` field.
7462
63+
## Dual package setup
64+
65+
The previously mentioned setup only works with tools that support ES modules. If you want to support tools that don't support ESM and use the CommonJS module system, you can set up a dual package setup.
66+
67+
A dual package setup means that you have 2 builds of your library: one for ESM and one for CommonJS. The ESM build is used by tools that support ES modules, while the CommonJS build is used by tools that don't support ES modules.
68+
69+
To configure a dual package setup, you can follow these steps:
70+
71+
1. Add the `commonjs` target to the `react-native-builder-bob` field in your `package.json` or `bob.config.js`:
72+
73+
```diff
74+
"react-native-builder-bob": {
75+
"source": "src",
76+
"output": "lib",
77+
"targets": [
78+
["module", { "esm": true }],
79+
+ ["commonjs", { "esm": true }]
80+
"typescript",
81+
]
82+
}
83+
```
84+
85+
2. Optionally change the `main` field in your `package.json` to point to the CommonJS build:
86+
87+
```diff
88+
- "main": "./lib/module/index.js",
89+
+ "main": "./lib/commonjs/index.js",
90+
```
91+
92+
This is needed if you want to support tools that don't support the `exports` field and need to use the CommonJS build.
93+
94+
3. Optionally add a `module` field in your `package.json` to point to the ESM build:
95+
96+
```diff
97+
"main": "./lib/commonjs/index.js",
98+
+ "module": "./lib/module/index.js",
99+
```
100+
101+
The `module` field is a non-standard field that some tools use to determine the ESM entry point.
102+
103+
4. Change the `exports` field in your `package.json` to include 2 conditions:
104+
105+
```diff
106+
"exports": {
107+
".": {
108+
- "types": "./lib/typescript/src/index.d.ts",
109+
- "default": "./lib/module/index.js"
110+
+ "import": {
111+
+ "types": "./lib/typescript/module/src/index.d.ts",
112+
+ "default": "./lib/module/index.js"
113+
+ },
114+
+ "require": {
115+
+ "types": "./lib/typescript/commonjs/src/index.d.ts",
116+
+ "default": "./lib/commonjs/index.js"
117+
+ }
118+
}
119+
},
120+
```
121+
122+
Here, we specify 2 conditions:
123+
124+
- `import`: Used when the library is imported with an `import` statement or a dynamic `import()`. It will point to the ESM build.
125+
- `require`: Used when the library is required with a `require` call. It will point to the CommonJS build.
126+
127+
Each condition has a `types` field - necessary for TypeScript to provide the appropriate definitions for the module system. The type definitions have slightly different semantics for CommonJS and ESM, so it's important to specify them separately.
128+
129+
The `default` field is the fallback entry point for both conditions. It's used for the actual JS code when the library is imported or required.
130+
131+
> **Important:** With this approach, the ESM and CommonJS versions of the package are treated as separate modules by Node.js as they are different files, leading to [potential issues](https://nodejs.org/docs/latest-v19.x/api/packages.html#dual-package-hazard) if the package is both imported and required in the same runtime environment. If the package relies on any state that can cause issues if 2 separate instances are loaded, it's necessary to isolate the state into a separate CommonJS module that can be shared between the ESM and CommonJS builds.
132+
75133
## Guidelines
76134

77135
There are still a few things to keep in mind if you want your library to be ESM-compatible:
@@ -100,7 +158,19 @@ There are still a few things to keep in mind if you want your library to be ESM-
100158

101159
- 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.
102160
- Avoid using `"moduleResolution": "node16"` or `"moduleResolution": "nodenext"` in your `tsconfig.json` file. They require file extensions in import statements which breaks platform-specific extension resolution.
103-
- If you specify a `react-native` condition in `exports`, make sure that it comes before the `default` condition. The conditions should be ordered from the most specific to the least specific:
161+
- If you specify a `react-native` condition in `exports`, make sure that it comes before other conditions. The conditions should be ordered from the most specific to the least specific:
162+
163+
```json
164+
"exports": {
165+
".": {
166+
"types": "./lib/typescript/src/index.d.ts",
167+
"react-native": "./lib/modules/index.native.js",
168+
"default": "./lib/module/index.js"
169+
}
170+
}
171+
```
172+
173+
Or for a dual package setup:
104174

105175
```json
106176
"exports": {

packages/create-react-native-library/templates/common/$package.json

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,11 @@
33
"version": "0.1.0",
44
"description": "<%- project.description %>",
55
"source": "./src/index.tsx",
6-
"main": "./lib/commonjs/index.js",
7-
"module": "./lib/module/index.js",
6+
"main": "./lib/module/index.js",
87
"exports": {
98
".": {
10-
"import": {
11-
"types": "./lib/typescript/module/src/index.d.ts",
12-
"default": "./lib/module/index.js"
13-
},
14-
"require": {
15-
"types": "./lib/typescript/commonjs/src/index.d.ts",
16-
"default": "./lib/commonjs/index.js"
17-
}
9+
"types": "./lib/typescript/src/index.d.ts",
10+
"default": "./lib/module/index.js"
1811
},
1912
"./package.json": "./package.json"
2013
},
@@ -186,12 +179,6 @@
186179
"esm": true
187180
}
188181
],
189-
[
190-
"commonjs",
191-
{
192-
"esm": true
193-
}
194-
],
195182
[
196183
"typescript",
197184
{

packages/react-native-builder-bob/src/__tests__/__snapshots__/init.test.ts.snap

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,13 @@ exports[`initializes the configuration 1`] = `
99
},
1010
"exports": {
1111
".": {
12-
"import": {
13-
"types": "./lib/typescript/module/src/index.d.ts",
14-
"default": "./lib/module/index.js"
15-
},
16-
"require": {
17-
"types": "./lib/typescript/commonjs/src/index.d.ts",
18-
"default": "./lib/commonjs/index.js"
19-
}
12+
"types": "./lib/typescript/src/index.d.ts",
13+
"default": "./lib/module/index.js"
2014
},
2115
"./package.json": "./package.json"
2216
},
2317
"source": "./src/index.ts",
24-
"main": "./lib/commonjs/index.js",
25-
"module": "./lib/module/index.js",
18+
"main": "./lib/module/index.js",
2619
"scripts": {
2720
"prepare": "bob build"
2821
},
@@ -43,12 +36,6 @@ exports[`initializes the configuration 1`] = `
4336
"esm": true
4437
}
4538
],
46-
[
47-
"commonjs",
48-
{
49-
"esm": true
50-
}
51-
],
5239
"typescript"
5340
]
5441
},

packages/react-native-builder-bob/src/init.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export async function init() {
101101
{
102102
title: 'commonjs - for legacy setups (Node.js < 20)',
103103
value: 'commonjs',
104-
selected: true,
104+
selected: false,
105105
},
106106
{
107107
title: 'typescript - declaration files for typechecking',
@@ -299,14 +299,6 @@ export async function init() {
299299
entryFields.main = entries.module;
300300
}
301301

302-
if (targets.includes('typescript') && !pkg.exports?.['.']) {
303-
if (entryFields.main === entries.commonjs) {
304-
entryFields.types = types.require;
305-
} else {
306-
entryFields.types = types.import;
307-
}
308-
}
309-
310302
for (const key in entryFields) {
311303
const entry = entryFields[key as keyof typeof entryFields];
312304

packages/react-native-builder-bob/src/targets/typescript.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -257,12 +257,14 @@ export default async function build({
257257
name: "exports['.'].types",
258258
value: pkg.exports?.['.']?.types,
259259
output: outDir,
260-
error: Boolean(esm && variants.commonjs && variants.module),
261-
message: `using both ${kleur.blue('commonjs')} and ${kleur.blue(
262-
'module'
263-
)} targets with ${kleur.blue(
264-
'esm'
265-
)} option enabled. Specify ${kleur.blue(
260+
error: Boolean(
261+
pkg.exports?.['.']?.import && pkg.exports?.['.']?.require
262+
),
263+
message: `using ${kleur.blue(
264+
"exports['.'].import"
265+
)} and ${kleur.blue(
266+
"exports['.'].require"
267+
)}. Specify ${kleur.blue(
266268
"exports['.'].import.types"
267269
)} and ${kleur.blue("exports['.'].require.types")} instead.`,
268270
},

0 commit comments

Comments
 (0)