Skip to content

Commit fb1da66

Browse files
authored
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 066e851 commit fb1da66

File tree

13 files changed

+244
-128
lines changed

13 files changed

+244
-128
lines changed

.github/workflows/build-templates.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,6 @@ jobs:
200200
working-directory: ${{ env.work_dir }}
201201
run: |
202202
yarn typecheck
203-
# FIXME: Remove this once we fix the typecheck errors
204-
continue-on-error: true
205203
206204
- name: Test library
207205
working-directory: ${{ env.work_dir }}

docs/pages/build.md

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +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-
"react-native": "src/index.ts",
79-
"types": "lib/typescript/src/index.d.ts",
80-
"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+
},
8187
"files": [
8288
"lib",
8389
"src"
@@ -88,7 +94,6 @@ yarn add --dev react-native-builder-bob
8894

8995
- `main`: The entry point for the commonjs build. This is used by Node - such as tests, SSR etc.
9096
- `module`: The entry point for the ES module build. This is used by bundlers such as webpack.
91-
- `react-native`: The entry point for the React Native apps. This is used by Metro. It's common to point to the source code here as it can make debugging easier.
9297
- `types`: The entry point for the TypeScript definitions. This is used by TypeScript to type check the code using your library.
9398
- `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.
9499
- `files`: The files to include in the package when publishing with `npm`.
@@ -150,7 +155,7 @@ Various targets to build for. The available targets are:
150155

151156
Enable compiling source files with Babel and use commonjs module system.
152157

153-
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`.
158+
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` field of `package.json`.
154159

155160
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).
156161

@@ -174,7 +179,7 @@ Example:
174179

175180
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.
176181

177-
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`.
182+
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` field of `package.json`.
178183

179184
Example:
180185

@@ -198,6 +203,8 @@ Example:
198203
["typescript", { "project": "tsconfig.build.json" }]
199204
```
200205

206+
The output file should be referenced in the `types` field or `exports['.'].types` field of `package.json`.
207+
201208
## Commands
202209

203210
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-example/example/src/App.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import * as React from 'react';
2-
31
<% if (project.view) { -%>
42
import { StyleSheet, View } from 'react-native';
53
import { <%- project.name -%>View } from '<%- project.slug -%>';
64
<% } else { -%>
5+
<% if (project.arch !== 'new') { -%>
6+
import { useState, useEffect } from 'react';
7+
<% } -%>
78
import { StyleSheet, View, Text } from 'react-native';
89
import { multiply } from '<%- project.slug -%>';
910
<% } -%>
@@ -28,9 +29,9 @@ export default function App() {
2829
}
2930
<% } else { -%>
3031
export default function App() {
31-
const [result, setResult] = React.useState<number | undefined>();
32+
const [result, setResult] = useState<number | undefined>();
3233

33-
React.useEffect(() => {
34+
useEffect(() => {
3435
multiply(3, 7).then(setResult);
3536
}, []);
3637

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +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-
"react-native": "src/index",
9-
"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+
},
1016
"files": [
1117
"src",
1218
"lib",
@@ -130,6 +136,7 @@
130136
"prettier"
131137
],
132138
"rules": {
139+
"react/react-in-jsx-scope": "off",
133140
"prettier/prettier": [
134141
"error",
135142
{

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
"allowUnusedLabels": false,
99
"esModuleInterop": true,
1010
"forceConsistentCasingInFileNames": true,
11-
"jsx": "react",
12-
"lib": ["esnext"],
13-
"module": "esnext",
14-
"moduleResolution": "node",
11+
"jsx": "react-jsx",
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: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
const browserslist = require('browserslist');
44

55
module.exports = function (api, options, cwd) {
6+
const cjs = options.modules === 'commonjs';
7+
68
return {
79
presets: [
810
[
@@ -24,12 +26,25 @@ module.exports = function (api, options, cwd) {
2426
node: '18',
2527
},
2628
useBuiltIns: false,
27-
modules: options.modules || false,
29+
modules: cjs ? 'commonjs' : false,
30+
},
31+
],
32+
[
33+
require.resolve('@babel/preset-react'),
34+
{
35+
runtime: 'automatic',
2836
},
2937
],
30-
require.resolve('@babel/preset-react'),
3138
require.resolve('@babel/preset-typescript'),
3239
require.resolve('@babel/preset-flow'),
3340
],
41+
plugins: [
42+
[
43+
require.resolve('./lib/babel'),
44+
{
45+
extension: cjs ? 'cjs' : 'mjs',
46+
},
47+
],
48+
],
3449
};
3550
};

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

Lines changed: 79 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ yargs
2828
const { shouldContinue } = await prompts({
2929
type: 'confirm',
3030
name: 'shouldContinue',
31-
message: `The working directory is not clean. You should commit or stash your changes before configuring bob. Continue anyway?`,
31+
message: `The working directory is not clean.\n You should commit or stash your changes before configuring bob.\n Continue anyway?`,
3232
initial: false,
3333
});
3434

@@ -41,7 +41,7 @@ yargs
4141

4242
if (!(await fs.pathExists(pak))) {
4343
logger.exit(
44-
`Couldn't find a 'package.json' file in '${root}'. Are you in a project folder?`
44+
`Couldn't find a 'package.json' file in '${root}'.\n Are you in a project folder?`
4545
);
4646
}
4747

@@ -52,7 +52,7 @@ yargs
5252
const { shouldContinue } = await prompts({
5353
type: 'confirm',
5454
name: 'shouldContinue',
55-
message: `The project seems to be already configured with bob. Do you want to overwrite the existing configuration?`,
55+
message: `The project seems to be already configured with bob.\n Do you want to overwrite the existing configuration?`,
5656
initial: false,
5757
});
5858

@@ -81,7 +81,7 @@ yargs
8181

8282
if (!entryFile) {
8383
logger.exit(
84-
`Couldn't find a 'index.js'. 'index.ts' or 'index.tsx' file under '${source}'. Please re-run the CLI after creating it.`
84+
`Couldn't find a 'index.js'. 'index.ts' or 'index.tsx' file under '${source}'.\n Please re-run the CLI after creating it.`
8585
);
8686
return;
8787
}
@@ -147,26 +147,34 @@ yargs
147147
? targets[0]
148148
: undefined;
149149

150-
const entries: { [key: string]: string } = {
151-
'main': target
152-
? path.join(output, target, 'index.js')
153-
: path.join(source, entryFile),
154-
'react-native': path.join(source, entryFile),
155-
'source': path.join(source, entryFile),
150+
const entries: {
151+
[key in 'source' | 'main' | 'module' | 'types']?: string;
152+
} = {
153+
source: `./${path.join(source, entryFile)}`,
154+
main: `./${
155+
target
156+
? path.join(output, target, 'index.cjs')
157+
: path.join(source, entryFile)
158+
}`,
156159
};
157160

158161
if (targets.includes('module')) {
159-
entries.module = path.join(output, 'module', 'index.js');
162+
entries.module = `./${path.join(output, 'module', 'index.mjs')}`;
160163
}
161164

162165
if (targets.includes('typescript')) {
163-
entries.types = path.join(output, 'typescript', source, 'index.d.ts');
166+
entries.types = `./${path.join(
167+
output,
168+
'typescript',
169+
source,
170+
'index.d.ts'
171+
)}`;
164172

165173
if (!(await fs.pathExists(path.join(root, 'tsconfig.json')))) {
166174
const { tsconfig } = await prompts({
167175
type: 'confirm',
168176
name: 'tsconfig',
169-
message: `You have enabled 'typescript' compilation, but we couldn't find a 'tsconfig.json' in project root. Generate one?`,
177+
message: `You have enabled 'typescript' compilation, but we couldn't find a 'tsconfig.json' in project root.\n Generate one?`,
170178
initial: true,
171179
});
172180

@@ -180,10 +188,10 @@ yargs
180188
allowUnusedLabels: false,
181189
esModuleInterop: true,
182190
forceConsistentCasingInFileNames: true,
183-
jsx: 'react',
184-
lib: ['esnext'],
185-
module: 'esnext',
186-
moduleResolution: 'node',
191+
jsx: 'react-jsx',
192+
lib: ['ESNext'],
193+
module: 'ESNext',
194+
moduleResolution: 'Bundler',
187195
noFallthroughCasesInSwitch: true,
188196
noImplicitReturns: true,
189197
noImplicitUseStrict: false,
@@ -194,7 +202,7 @@ yargs
194202
resolveJsonModule: true,
195203
skipLibCheck: true,
196204
strict: true,
197-
target: 'esnext',
205+
target: 'ESNext',
198206
verbatimModuleSyntax: true,
199207
},
200208
},
@@ -214,13 +222,13 @@ yargs
214222
];
215223

216224
for (const key in entries) {
217-
const entry = entries[key];
225+
const entry = entries[key as keyof typeof entries];
218226

219227
if (pkg[key] && pkg[key] !== entry) {
220228
const { replace } = await prompts({
221229
type: 'confirm',
222230
name: 'replace',
223-
message: `Your package.json has the '${key}' field set to '${pkg[key]}'. Do you want to replace it with '${entry}'?`,
231+
message: `Your package.json has the '${key}' field set to '${pkg[key]}'.\n Do you want to replace it with '${entry}'?`,
224232
initial: true,
225233
});
226234

@@ -232,11 +240,60 @@ yargs
232240
}
233241
}
234242

243+
if (Object.values(entries).some((entry) => entry.endsWith('.mjs'))) {
244+
let replace = false;
245+
246+
const exports = {
247+
'.': {
248+
...(entries.types ? { types: entries.types } : null),
249+
...(entries.module ? { import: entries.module } : null),
250+
...(entries.main ? { require: entries.main } : null),
251+
},
252+
};
253+
254+
if (
255+
pkg.exports &&
256+
JSON.stringify(pkg.exports) !== JSON.stringify(exports)
257+
) {
258+
replace = (
259+
await prompts({
260+
type: 'confirm',
261+
name: 'replace',
262+
message: `Your package.json has 'exports' field set.\n Do you want to replace it?`,
263+
initial: true,
264+
})
265+
).replace;
266+
} else {
267+
replace = true;
268+
}
269+
270+
if (replace) {
271+
pkg.exports = exports;
272+
}
273+
}
274+
275+
if (
276+
pkg['react-native'] &&
277+
(pkg['react-native'].startsWith(source) ||
278+
pkg['react-native'].startsWith(`./${source}`))
279+
) {
280+
const { remove } = await prompts({
281+
type: 'confirm',
282+
name: 'remove',
283+
message: `Your package.json has the 'react-native' field pointing to source code.\n This can cause problems when customizing babel configuration.\n Do you want to remove it?`,
284+
initial: true,
285+
});
286+
287+
if (remove) {
288+
delete pkg['react-native'];
289+
}
290+
}
291+
235292
if (pkg.scripts?.prepare && pkg.scripts.prepare !== prepare) {
236293
const { replace } = await prompts({
237294
type: 'confirm',
238295
name: 'replace',
239-
message: `Your package.json has the 'scripts.prepare' field set to '${pkg.scripts.prepare}'. Do you want to replace it with '${prepare}'?`,
296+
message: `Your package.json has the 'scripts.prepare' field set to '${pkg.scripts.prepare}'.\n Do you want to replace it with '${prepare}'?`,
240297
initial: true,
241298
});
242299

@@ -256,7 +313,7 @@ yargs
256313
const { update } = await prompts({
257314
type: 'confirm',
258315
name: 'update',
259-
message: `Your package.json already has a 'files' field. Do you want to update it?`,
316+
message: `Your package.json already has a 'files' field.\n Do you want to update it?`,
260317
initial: true,
261318
});
262319

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,5 @@ export default async function build({
3636
exclude,
3737
modules: 'commonjs',
3838
report,
39-
field: 'main',
4039
});
4140
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,5 @@ export default async function build({
3636
exclude,
3737
modules: false,
3838
report,
39-
field: 'module',
4039
});
4140
}

0 commit comments

Comments
 (0)