Skip to content

Commit 9648f45

Browse files
committed
feat: add esm build option for typescript
1 parent cc30f7f commit 9648f45

File tree

6 files changed

+202
-67
lines changed

6 files changed

+202
-67
lines changed

docs/pages/build.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ yarn add --dev react-native-builder-bob
4444
"targets": [
4545
["commonjs", { "esm": true }],
4646
["module", { "esm": true }],
47-
"typescript",
47+
["typescript", { "esm": true }]
4848
]
4949
}
5050
```
@@ -76,12 +76,16 @@ yarn add --dev react-native-builder-bob
7676
"source": "./src/index.tsx",
7777
"main": "./lib/commonjs/index.js",
7878
"module": "./lib/module/index.js",
79-
"types": "./lib/typescript/src/index.d.ts",
8079
"exports": {
8180
".": {
82-
"types": "./typescript/src/index.d.ts",
83-
"import": "./module/index.js",
84-
"require": "./commonjs/index.js"
81+
"import": {
82+
"types": "./lib/typescript/module/src/index.d.ts",
83+
"default": "./lib/module/index.js"
84+
},
85+
"require": {
86+
"types": "./lib/typescript/commonjs/src/index.d.ts",
87+
"default": "./lib/commonjs/index.js"
88+
}
8589
}
8690
},
8791
"files": [
@@ -224,6 +228,12 @@ Example:
224228

225229
The output file should be referenced in the `types` field or `exports['.'].types` field of `package.json`.
226230

231+
##### `esm`
232+
233+
Setting this option to `true` will output 2 sets of type definitions: one for the CommonJS build and one for the ES module build.
234+
235+
See the [ESM support](./esm.md) guide for more details.
236+
227237
## Commands
228238

229239
The `bob` CLI exposes the following commands:

docs/pages/esm.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ You can verify whether ESM support is enabled by checking the configuration for
1111
"targets": [
1212
["commonjs", { "esm": true }],
1313
["module", { "esm": true }],
14-
"typescript",
14+
["typescript", { "esm": true }]
1515
]
1616
}
1717
```
1818

19-
The `"esm": true` option enables ESM-compatible output by adding the `.js` extension to the import statements in the generated files.
19+
The `"esm": true` option enables ESM-compatible output by adding the `.js` extension to the import statements in the generated files. For TypeScript, it also generates 2 sets of type definitions: one for the CommonJS build and one for the ES module build.
2020

2121
It's recommended to specify `"moduleResolution": "Bundler"` in your `tsconfig.json` file as well:
2222

@@ -43,10 +43,16 @@ There are still a few things to keep in mind if you want your library to be ESM-
4343
```json
4444
"exports": {
4545
".": {
46-
"types": "./lib/typescript/src/index.d.ts",
47-
"react-native": "./lib/modules/index.native.js",
48-
"import": "./lib/modules/index.js",
49-
"require": "./lib/commonjs/index.js"
46+
"import": {
47+
"types": "./lib/typescript/module/src/index.d.ts",
48+
"react-native": "./lib/modules/index.native.js",
49+
"default": "./lib/module/index.js"
50+
},
51+
"require": {
52+
"types": "./lib/typescript/commonjs/src/index.d.ts",
53+
"react-native": "./lib/commonjs/index.native.js",
54+
"default": "./lib/commonjs/index.js"
55+
}
5056
}
5157
}
5258
```

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.28.0';
17+
const FALLBACK_BOB_VERSION = '0.29.0';
1818

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

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@
55
"source": "./src/index.tsx",
66
"main": "./lib/commonjs/index.js",
77
"module": "./lib/module/index.js",
8-
"types": "./lib/typescript/src/index.d.ts",
98
"exports": {
109
".": {
11-
"types": "./lib/typescript/src/index.d.ts",
12-
"import": "./lib/module/index.js",
13-
"require": "./lib/commonjs/index.js"
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+
}
1418
}
1519
},
1620
"files": [
@@ -178,7 +182,8 @@
178182
[
179183
"typescript",
180184
{
181-
"project": "tsconfig.build.json"
185+
"project": "tsconfig.build.json",
186+
"esm": true
182187
}
183188
]
184189
]

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

Lines changed: 130 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,18 @@ import { platform } from 'os';
99
import type { Input } from '../types';
1010

1111
type Options = Input & {
12-
options?: { project?: string; tsc?: string };
12+
options?: {
13+
esm?: boolean;
14+
project?: string;
15+
tsc?: string;
16+
};
17+
};
18+
19+
type Field = {
20+
name: string;
21+
value: string | undefined;
22+
output: string | undefined;
23+
error: boolean;
1324
};
1425

1526
export default async function build({
@@ -156,6 +167,13 @@ export default async function build({
156167
// Ignore
157168
}
158169

170+
const outputs = options?.esm
171+
? {
172+
commonjs: path.join(output, 'commonjs'),
173+
module: path.join(output, 'module'),
174+
}
175+
: { commonjs: output };
176+
159177
const result = spawn.sync(
160178
tsc,
161179
[
@@ -168,7 +186,7 @@ export default async function build({
168186
'--project',
169187
project,
170188
'--outDir',
171-
output,
189+
outputs.commonjs,
172190
],
173191
{
174192
stdio: 'inherit',
@@ -179,6 +197,18 @@ export default async function build({
179197
if (result.status === 0) {
180198
await del([tsbuildinfo]);
181199

200+
if (outputs?.module) {
201+
// When ESM compatible output is enabled, we need to generate 2 builds for commonjs and esm
202+
// In this case we copy the already generated types, and add `package.json` with `type` field
203+
await fs.copy(outputs.commonjs, outputs.module);
204+
await fs.writeJSON(path.join(outputs.commonjs, 'package.json'), {
205+
type: 'commonjs',
206+
});
207+
await fs.writeJSON(path.join(outputs.module, 'package.json'), {
208+
type: 'module',
209+
});
210+
}
211+
182212
report.success(
183213
`Wrote definition files to ${kleur.blue(path.relative(root, output))}`
184214
);
@@ -187,16 +217,52 @@ export default async function build({
187217
await fs.readFile(path.join(root, 'package.json'), 'utf-8')
188218
);
189219

190-
const getGeneratedTypesPath = async () => {
220+
const fields: Field[] = [
221+
...(pkg.exports?.['.']?.types
222+
? [
223+
{
224+
name: "exports['.'].types",
225+
value: pkg.exports?.['.']?.types,
226+
output: outputs.commonjs,
227+
error: options?.esm === true,
228+
},
229+
]
230+
: [
231+
{
232+
name: 'types',
233+
value: pkg.types,
234+
output: outputs.commonjs,
235+
error: options?.esm === true,
236+
},
237+
]),
238+
{
239+
name: "exports['.'].import.types",
240+
value: pkg.exports?.['.']?.import?.types,
241+
output: outputs.module,
242+
error: !options?.esm,
243+
},
244+
{
245+
name: "exports['.'].require.types",
246+
value: pkg.exports?.['.']?.require?.types,
247+
output: outputs.commonjs,
248+
error: !options?.esm,
249+
},
250+
];
251+
252+
const getGeneratedTypesPath = async (field: Field) => {
253+
if (!field.output || field.error) {
254+
return null;
255+
}
256+
191257
if (pkg.source) {
192258
const indexDTsName =
193259
path.basename(pkg.source).replace(/\.(jsx?|tsx?)$/, '') + '.d.ts';
194260

195261
const potentialPaths = [
196-
path.join(output, path.dirname(pkg.source), indexDTsName),
262+
path.join(field.output, path.dirname(pkg.source), indexDTsName),
197263
path.join(
198-
output,
199-
path.dirname(path.relative(source, path.join(root, pkg.source))),
264+
field.output,
265+
path.relative(source, path.join(root, path.dirname(pkg.source))),
200266
indexDTsName
201267
),
202268
];
@@ -211,37 +277,63 @@ export default async function build({
211277
return null;
212278
};
213279

214-
const fields = [
215-
{ name: 'types', value: pkg.types },
216-
{ name: "exports['.'].types", value: pkg.exports?.['.']?.types },
217-
];
218-
219-
if (fields.some((field) => field.value)) {
280+
const invalidFieldNames = (
220281
await Promise.all(
221-
fields.map(async ({ name, value }) => {
222-
if (!value) {
223-
return;
282+
fields.map(async (field) => {
283+
if (field.error) {
284+
if (field.value) {
285+
report.error(
286+
`The ${kleur.blue(field.name)} field in ${kleur.blue(
287+
`package.json`
288+
)} should not be set when the ${kleur.blue(
289+
'esm'
290+
)} option is ${options?.esm ? 'enabled' : 'disabled'}.`
291+
);
292+
293+
return field.name;
294+
}
295+
296+
return null;
224297
}
225298

226-
const typesPath = path.join(root, value);
299+
if (
300+
field.name.startsWith('exports') &&
301+
field.value &&
302+
!/^\.\//.test(field.value)
303+
) {
304+
report.warn(
305+
`The ${kleur.blue(field.name)} field in ${kleur.blue(
306+
`package.json`
307+
)} should be a relative path starting with ${kleur.blue(
308+
'./'
309+
)}. Found: ${kleur.blue(field.value)}`
310+
);
227311

228-
if (!(await fs.pathExists(typesPath))) {
229-
const generatedTypesPath = await getGeneratedTypesPath();
312+
return field.name;
313+
}
230314

231-
if (!generatedTypesPath) {
232-
report.warn(
233-
`Failed to detect the entry point for the generated types. Make sure you have a valid ${kleur.blue(
234-
'source'
235-
)} field in your ${kleur.blue('package.json')}.`
236-
);
237-
}
315+
const generatedTypesPath = await getGeneratedTypesPath(field);
316+
const isValid =
317+
field.value && generatedTypesPath
318+
? path.join(root, field.value) ===
319+
path.join(root, generatedTypesPath)
320+
: false;
321+
322+
if (!isValid) {
323+
const type =
324+
field.value &&
325+
(await fs.pathExists(path.join(root, field.value)))
326+
? 'invalid'
327+
: 'non-existent';
238328

239329
report.error(
240-
`The ${kleur.blue(name)} field in ${kleur.blue(
241-
'package.json'
242-
)} points to a non-existent file: ${kleur.blue(
243-
value
244-
)}.\nVerify the path points to the correct file under ${kleur.blue(
330+
`The ${kleur.blue(field.name)} field ${
331+
field.value
332+
? `in ${kleur.blue(
333+
'package.json'
334+
)} points to a ${type} file: ${kleur.blue(field.value)}`
335+
: `is missing in ${kleur.blue('package.json')}`
336+
}.\nVerify the path points to the correct file under ${kleur.blue(
245337
path.relative(root, output)
246338
)}${
247339
generatedTypesPath
@@ -250,21 +342,17 @@ export default async function build({
250342
}`
251343
);
252344

253-
throw new Error(`Found incorrect path in '${name}' field.`);
345+
return field.name;
254346
}
347+
348+
return null;
255349
})
256-
);
257-
} else {
258-
const generatedTypesPath = await getGeneratedTypesPath();
350+
)
351+
).filter((name): name is string => name != null);
259352

260-
report.warn(
261-
`No ${kleur.blue(
262-
fields.map((field) => field.name).join(' or ')
263-
)} field found in ${kleur.blue('package.json')}.\nConsider ${
264-
generatedTypesPath
265-
? `pointing it to ${kleur.blue(generatedTypesPath)}`
266-
: 'adding it'
267-
} so that consumers of your package can use the types.`
353+
if (invalidFieldNames.length) {
354+
throw new Error(
355+
`Found errors for fields: ${invalidFieldNames.join(', ')}.`
268356
);
269357
}
270358
} else {

0 commit comments

Comments
 (0)