Skip to content

Commit 450e12a

Browse files
committed
feat: add ESM support for generated project
1 parent 693a27b commit 450e12a

File tree

10 files changed

+207
-110
lines changed

10 files changed

+207
-110
lines changed

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.ts",
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
@@ -11,7 +11,7 @@ import prompts, { type PromptObject } from './utils/prompts';
1111
import generateExampleApp from './utils/generateExampleApp';
1212
import { spawn } from './utils/spawn';
1313

14-
const FALLBACK_BOB_VERSION = '0.20.0';
14+
const FALLBACK_BOB_VERSION = '0.24.0';
1515

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

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

Lines changed: 11 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.ts",
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",

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",
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: 11 additions & 1 deletion
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,20 @@ module.exports = function (api, options, cwd) {
2426
node: '18',
2527
},
2628
useBuiltIns: false,
27-
modules: options.modules || false,
29+
modules: cjs ? 'commonjs' : false,
2830
},
2931
],
3032
require.resolve('@babel/preset-react'),
3133
require.resolve('@babel/preset-typescript'),
3234
require.resolve('@babel/preset-flow'),
3335
],
36+
plugins: [
37+
[
38+
require.resolve('./lib/babel'),
39+
{
40+
extension: cjs ? 'cjs' : 'mjs',
41+
},
42+
],
43+
],
3444
};
3545
};

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

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -147,20 +147,28 @@ 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({
@@ -181,9 +189,9 @@ yargs
181189
esModuleInterop: true,
182190
forceConsistentCasingInFileNames: true,
183191
jsx: 'react',
184-
lib: ['esnext'],
185-
module: 'esnext',
186-
moduleResolution: 'node',
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,7 +222,7 @@ 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({
@@ -232,6 +240,41 @@ yargs
232240
}
233241
}
234242

243+
if (Object.values(entries).some((entry) => entry.endsWith('.mjs'))) {
244+
let replace = false;
245+
246+
if (pkg.exports) {
247+
replace = (
248+
await prompts({
249+
type: 'confirm',
250+
name: 'replace',
251+
message: `Your package.json has 'exports' field set. Do you want to replace it?`,
252+
initial: true,
253+
})
254+
).replace;
255+
} else {
256+
replace = true;
257+
}
258+
259+
if (replace) {
260+
pkg.exports = {
261+
'.': {},
262+
};
263+
264+
if (entries.types) {
265+
pkg.exports['.'].types = entries.types;
266+
}
267+
268+
if (entries.module) {
269+
pkg.exports['.'].import = entries.module;
270+
}
271+
272+
if (entries.main) {
273+
pkg.exports['.'].require = entries.main;
274+
}
275+
}
276+
}
277+
235278
if (pkg.scripts?.prepare && pkg.scripts.prepare !== prepare) {
236279
const { replace } = await prompts({
237280
type: 'confirm',

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
}

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

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -217,43 +217,56 @@ export default async function build({
217217
return null;
218218
};
219219

220-
if ('types' in pkg) {
221-
const typesPath = path.join(root, pkg.types);
222-
223-
if (!(await fs.pathExists(typesPath))) {
224-
const generatedTypesPath = await getGeneratedTypesPath();
225-
226-
if (!generatedTypesPath) {
227-
report.warn(
228-
`Failed to detect the entry point for the generated types. Make sure you have a valid ${kleur.blue(
229-
'source'
230-
)} field in your ${kleur.blue('package.json')}.`
231-
);
232-
}
233-
234-
report.error(
235-
`The ${kleur.blue('types')} field in ${kleur.blue(
236-
'package.json'
237-
)} points to a non-existent file: ${kleur.blue(
238-
pkg.types
239-
)}.\nVerify the path points to the correct file under ${kleur.blue(
240-
path.relative(root, output)
241-
)}${
242-
generatedTypesPath
243-
? ` (found ${kleur.blue(generatedTypesPath)}).`
244-
: '.'
245-
}`
246-
);
220+
const fields = [
221+
{ name: 'types', value: pkg.types },
222+
{ name: "exports['.'].types", value: pkg.exports?.['.']?.types },
223+
];
224+
225+
if (fields.some((field) => field.value)) {
226+
await Promise.all(
227+
fields.map(async ({ name, value }) => {
228+
if (!value) {
229+
return;
230+
}
247231

248-
throw new Error("Found incorrect path in 'types' field.");
249-
}
232+
const typesPath = path.join(root, value);
233+
234+
if (!(await fs.pathExists(typesPath))) {
235+
const generatedTypesPath = await getGeneratedTypesPath();
236+
237+
if (!generatedTypesPath) {
238+
report.warn(
239+
`Failed to detect the entry point for the generated types. Make sure you have a valid ${kleur.blue(
240+
'source'
241+
)} field in your ${kleur.blue('package.json')}.`
242+
);
243+
}
244+
245+
report.error(
246+
`The ${kleur.blue(name)} field in ${kleur.blue(
247+
'package.json'
248+
)} points to a non-existent file: ${kleur.blue(
249+
value
250+
)}.\nVerify the path points to the correct file under ${kleur.blue(
251+
path.relative(root, output)
252+
)}${
253+
generatedTypesPath
254+
? ` (found ${kleur.blue(generatedTypesPath)}).`
255+
: '.'
256+
}`
257+
);
258+
259+
throw new Error(`Found incorrect path in '${name}' field.`);
260+
}
261+
})
262+
);
250263
} else {
251264
const generatedTypesPath = await getGeneratedTypesPath();
252265

253266
report.warn(
254-
`No ${kleur.blue('types')} field found in ${kleur.blue(
255-
'package.json'
256-
)}.\nConsider ${
267+
`No ${kleur.blue(
268+
fields.map((field) => field.name).join(' or ')
269+
)} field found in ${kleur.blue('package.json')}.\nConsider ${
257270
generatedTypesPath
258271
? `pointing it to ${kleur.blue(generatedTypesPath)}`
259272
: 'adding it'

0 commit comments

Comments
 (0)