Skip to content

Commit 933a3b3

Browse files
committed
feat: add ESM support for generated project (#583)
This adds ESM support to the generated project. To do this: - Use `.cjs` and `.mjs` file extensions for the generated files - Add file extensions to imports in the compiled code - Add the `exports` field in `package.json` - Update the `moduleResolution` config to `Bundler` in `tsconfig.json` In addition: - Enable the new JSX runtime option for React - Recommend removing the `react-native` field from `package.json` This is a breaking change for library authors. After upgrading, it's necessary to update the configuration by running the following command: ```sh yarn bob init ``` Alternatively, they can follow the [manual configuration guide](https://callstack.github.io/react-native-builder-bob/build#manual-configuration). In addition, typescript consumers would need to change the following fields in `tsconfig.json`: ```json "jsx": "react-jsx", "moduleResolution": "Bundler", ``` If using ESLint, it may also be necessary to disable the "react/react-in-jsx-scope" rule: ```json "react/react-in-jsx-scope": "off" ```
1 parent 0595213 commit 933a3b3

File tree

12 files changed

+342
-217
lines changed

12 files changed

+342
-217
lines changed

docs/pages/build.md

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ yarn add --dev react-native-builder-bob
4242
"source": "src",
4343
"output": "lib",
4444
"targets": [
45-
"commonjs",
46-
"module",
45+
["commonjs", { "esm" : true }],
46+
["module", { "esm" : true }],
4747
"typescript",
4848
]
4949
}
@@ -73,10 +73,17 @@ yarn add --dev react-native-builder-bob
7373
1. Configure the appropriate entry points:
7474

7575
```json
76-
"main": "lib/commonjs/index.js",
77-
"module": "lib/module/index.js",
78-
"types": "lib/typescript/src/index.d.ts",
79-
"source": "src/index.ts",
76+
"source": "./src/index.tsx",
77+
"main": "./lib/commonjs/index.cjs",
78+
"module": "./lib/module/index.mjs",
79+
"types": "./lib/typescript/src/index.d.ts",
80+
"exports": {
81+
".": {
82+
"types": "./typescript/src/index.d.ts",
83+
"require": "./commonjs/index.cjs",
84+
"import": "./module/index.mjs"
85+
}
86+
},
8087
"files": [
8188
"lib",
8289
"src"
@@ -85,15 +92,18 @@ yarn add --dev react-native-builder-bob
8592

8693
Here is what each of these fields mean:
8794

95+
- `source`: The path to the source code. It is used by `react-native-builder-bob` to detect the correct output files and provide better error messages.
8896
- `main`: The entry point for the commonjs build. This is used by Node - such as tests, SSR etc.
8997
- `module`: The entry point for the ES module build. This is used by bundlers such as webpack.
9098
- `types`: The entry point for the TypeScript definitions. This is used by TypeScript to type check the code using your library.
91-
- `source`: The path to the source code. It is used by `react-native-builder-bob` to detect the correct output files and provide better error messages.
9299
- `files`: The files to include in the package when publishing with `npm`.
100+
- `exports`: The entry points for tools that support the `exports` field in `package.json` - such as Node.js 12+ & modern browsers.
93101

94102
Make sure to change specify correct files according to the targets you have enabled.
95103

96-
**NOTE**: If you're building TypeScript definition files, also make sure that the `types` field points to a correct path. Depending on the project configuration, the path can be different for you than the example snippet (e.g. `lib/typescript/index.d.ts` if you have only the `src` directory and `rootDir` is not set).
104+
> The `exports` field also requires the `esm` option to be enabled for the [`commonjs`](#commonjs) and [`module`](#module) targets. In addition, the file extensions of the generated files will be `.js` instead of `.cjs` and `.mjs` if the `esm` option is not enabled.
105+
106+
> If you're building TypeScript definition files, also make sure that the `types` field points to a correct path. Depending on the project configuration, the path can be different for you than the example snippet (e.g. `lib/typescript/index.d.ts` if you have only the `src` directory and `rootDir` is not set).
97107
98108
1. Add the output directory to `.gitignore` and `.eslintignore`
99109

@@ -148,12 +158,14 @@ Various targets to build for. The available targets are:
148158

149159
Enable compiling source files with Babel and use commonjs module system.
150160

151-
This is useful for running the code in Node (SSR, tests etc.). The output file should be referenced in the `main` field of `package.json`.
161+
This is useful for running the code in Node (SSR, tests etc.). The output file should be referenced in the `main` field and `exports['.'].require` (when `esm: true`) field of `package.json`.
152162

153-
By default, the code is compiled to support last 2 versions of modern browsers. It also strips TypeScript and Flow annotations, and compiles JSX. You can customize the environments to compile for by using a [browserslist config](https://github.com/browserslist/browserslist#config-file).
163+
By default, the code is compiled to support the last 2 versions of modern browsers. It also strips TypeScript and Flow annotations as well as compiles JSX. You can customize the environments to compile for by using a [browserslist config](https://github.com/browserslist/browserslist#config-file).
154164

155165
In addition, the following options are supported:
156166

167+
- `esm` (`boolean`): Enabling this option will output ES modules compatible code for Node.js 12+, modern browsers and other tools that support `package.json`'s `exports` field. This mainly adds file extensions to the imports and exports. Note that file extensions are not added when importing a file that may have platform-specific extensions (e.g. `.android.ts`). In addition, the generated files will have `.cjs` (commonjs) and `.mjs` (modules) extensions instead of `.js` - this is necessary to disambiguate between the two formats.
168+
157169
- `configFile` (`boolean` | `string`): To customize the babel config used, you can pass the [`configFile`](https://babeljs.io/docs/en/options#configfile) option as `true` if you have a `babel.config.js` or a path to a custom config file. This will override the default configuration. You can extend the default configuration by using the [`react-native-builder-bob/babel-preset`](https://github.com/callstack/react-native-builder-bob/blob/main/packages/react-native-builder-bob/babel-preset.js) preset.
158170

159171
- `babelrc` (`boolean`): You can set the [`babelrc`](https://babeljs.io/docs/en/options#babelrc) option to `true` to enable using `.babelrc` files.
@@ -165,19 +177,19 @@ In addition, the following options are supported:
165177
Example:
166178

167179
```json
168-
["commonjs", { "configFile": true, "copyFlow": true }]
180+
["commonjs", { "esm": true, "copyFlow": true }]
169181
```
170182

171183
#### `module`
172184

173185
Enable compiling source files with Babel and use ES module system. This is essentially same as the `commonjs` target and accepts the same options, but leaves the `import`/`export` statements in your code.
174186

175-
This is useful for bundlers which understand ES modules and can tree-shake. The output file should be referenced in the `module` field of `package.json`.
187+
This is useful for bundlers which understand ES modules and can tree-shake. The output file should be referenced in the `module` field and `exports['.'].import` (when `esm: true`) field of `package.json`.
176188

177189
Example:
178190

179191
```json
180-
["module", { "configFile": true }]
192+
["module", { "esm": true, "sourceMaps": false }]
181193
```
182194

183195
#### `typescript`
@@ -196,6 +208,8 @@ Example:
196208
["typescript", { "project": "tsconfig.build.json" }]
197209
```
198210

211+
The output file should be referenced in the `types` field or `exports['.'].types` field of `package.json`.
212+
199213
## Commands
200214

201215
The `bob` CLI exposes the following commands:

packages/create-react-native-library/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import generateExampleApp, {
1414
import { spawn } from './utils/spawn';
1515
import { version } from '../package.json';
1616

17-
const FALLBACK_BOB_VERSION = '0.20.0';
17+
const FALLBACK_BOB_VERSION = '0.25.0';
1818

1919
const BINARIES = [
2020
/(gradlew|\.(jar|keystore|png|jpg|gif))$/,

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

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@
22
"name": "<%- project.slug -%>",
33
"version": "0.1.0",
44
"description": "<%- project.description %>",
5-
"main": "lib/commonjs/index",
6-
"module": "lib/module/index",
7-
"types": "lib/typescript/src/index.d.ts",
8-
"source": "src/index",
5+
"source": "./src/index.tsx",
6+
"main": "./lib/commonjs/index.cjs",
7+
"module": "./lib/module/index.mjs",
8+
"types": "./lib/typescript/src/index.d.ts",
9+
"exports": {
10+
".": {
11+
"types": "./lib/typescript/src/index.d.ts",
12+
"import": "./lib/module/index.mjs",
13+
"require": "./lib/commonjs/index.cjs"
14+
}
15+
},
916
"files": [
1017
"src",
1118
"lib",
@@ -157,8 +164,17 @@
157164
"source": "src",
158165
"output": "lib",
159166
"targets": [
160-
"commonjs",
161-
"module",
167+
[
168+
"commonjs",
169+
{
170+
"esm": true
171+
}
172+
],
173+
["module",
174+
{
175+
"esm": true
176+
}
177+
],
162178
[
163179
"typescript",
164180
{

packages/create-react-native-library/templates/common/tsconfig.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
"esModuleInterop": true,
1010
"forceConsistentCasingInFileNames": true,
1111
"jsx": "react-jsx",
12-
"lib": ["esnext"],
13-
"module": "esnext",
14-
"moduleResolution": "node",
12+
"lib": ["ESNext"],
13+
"module": "ESNext",
14+
"moduleResolution": "Bundler",
1515
"noFallthroughCasesInSwitch": true,
1616
"noImplicitReturns": true,
1717
"noImplicitUseStrict": false,
@@ -22,7 +22,7 @@
2222
"resolveJsonModule": true,
2323
"skipLibCheck": true,
2424
"strict": true,
25-
"target": "esnext",
25+
"target": "ESNext",
2626
"verbatimModuleSyntax": true
2727
}
2828
}

packages/react-native-builder-bob/babel-preset.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22

33
const browserslist = require('browserslist');
44

5+
/**
6+
* Babel preset for React Native Builder Bob
7+
* @param {'commonjs' | 'preserve'} options.modules - Whether to compile modules to CommonJS or preserve them
8+
* @param {Boolean} options.esm - Whether to output ES module compatible code, e.g. by adding extension to import/export statements
9+
*/
510
module.exports = function (api, options, cwd) {
11+
const cjs = options.modules === 'commonjs';
12+
613
return {
714
presets: [
815
[
@@ -24,7 +31,7 @@ module.exports = function (api, options, cwd) {
2431
node: '18',
2532
},
2633
useBuiltIns: false,
27-
modules: options.modules || false,
34+
modules: cjs ? 'commonjs' : false,
2835
},
2936
],
3037
[
@@ -36,5 +43,13 @@ module.exports = function (api, options, cwd) {
3643
require.resolve('@babel/preset-typescript'),
3744
require.resolve('@babel/preset-flow'),
3845
],
46+
plugins: [
47+
[
48+
require.resolve('./lib/babel'),
49+
{
50+
extension: options.esm ? (cjs ? 'cjs' : 'mjs') : undefined,
51+
},
52+
],
53+
],
3954
};
4055
};

packages/react-native-builder-bob/src/__tests__/index.test.ts

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { transformFileAsync } from '@babel/core';
33
import fs from 'node:fs';
44
import path from 'node:path';
55

6-
it.each(['imports', 'exports'])(`adds .js extension to %s`, async (name) => {
6+
it.each(['imports', 'exports'])(`adds extension to %s`, async (name) => {
77
const filepath = path.resolve(
88
__dirname,
99
`../__fixtures__/project/code/$${name}-input.ts`
@@ -25,36 +25,3 @@ it.each(['imports', 'exports'])(`adds .js extension to %s`, async (name) => {
2525

2626
expect(result?.code).toEqual(expected.trim());
2727
});
28-
29-
it('replaces alias imports', async () => {
30-
const filepath = path.resolve(
31-
__dirname,
32-
`../__fixtures__/project/code/$alias-input.ts`
33-
);
34-
35-
const result = await transformFileAsync(filepath, {
36-
cwd: __dirname,
37-
configFile: false,
38-
babelrc: false,
39-
plugins: [
40-
'@babel/plugin-syntax-typescript',
41-
[
42-
require.resolve('../babel.ts'),
43-
{
44-
alias: {
45-
'@': '../__fixtures__/project',
46-
'file': '../__fixtures__/project/f',
47-
'something': 'another',
48-
},
49-
},
50-
],
51-
],
52-
});
53-
54-
const expected = await fs.promises.readFile(
55-
path.resolve(__dirname, `../__fixtures__/project/code/$alias-output.ts`),
56-
'utf8'
57-
);
58-
59-
expect(result?.code).toEqual(expected.trim());
60-
});

0 commit comments

Comments
 (0)