Skip to content

Commit

Permalink
feat: add ESM support for generated project (#583)
Browse files Browse the repository at this point in the history
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"
```
  • Loading branch information
satya164 authored Jul 4, 2024
1 parent 066e851 commit fb1da66
Show file tree
Hide file tree
Showing 13 changed files with 244 additions and 128 deletions.
2 changes: 0 additions & 2 deletions .github/workflows/build-templates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,6 @@ jobs:
working-directory: ${{ env.work_dir }}
run: |
yarn typecheck
# FIXME: Remove this once we fix the typecheck errors
continue-on-error: true
- name: Test library
working-directory: ${{ env.work_dir }}
Expand Down
23 changes: 15 additions & 8 deletions docs/pages/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,17 @@ yarn add --dev react-native-builder-bob
1. Configure the appropriate entry points:

```json
"main": "lib/commonjs/index.js",
"module": "lib/module/index.js",
"react-native": "src/index.ts",
"types": "lib/typescript/src/index.d.ts",
"source": "src/index.ts",
"source": "./src/index.tsx",
"main": "./lib/commonjs/index.cjs",
"module": "./lib/module/index.mjs",
"types": "./lib/typescript/src/index.d.ts",
"exports": {
".": {
"types": "./typescript/src/index.d.ts",
"require": "./commonjs/index.cjs",
"import": "./module/index.mjs"
}
},
"files": [
"lib",
"src"
Expand All @@ -88,7 +94,6 @@ yarn add --dev react-native-builder-bob

- `main`: The entry point for the commonjs build. This is used by Node - such as tests, SSR etc.
- `module`: The entry point for the ES module build. This is used by bundlers such as webpack.
- `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.
- `types`: The entry point for the TypeScript definitions. This is used by TypeScript to type check the code using your library.
- `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.
- `files`: The files to include in the package when publishing with `npm`.
Expand Down Expand Up @@ -150,7 +155,7 @@ Various targets to build for. The available targets are:

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

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`.
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`.

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

Expand All @@ -174,7 +179,7 @@ Example:

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.

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`.
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`.

Example:

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

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

## Commands

The `bob` CLI exposes the following commands:
Expand Down
2 changes: 1 addition & 1 deletion packages/create-react-native-library/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import generateExampleApp, {
import { spawn } from './utils/spawn';
import { version } from '../package.json';

const FALLBACK_BOB_VERSION = '0.20.0';
const FALLBACK_BOB_VERSION = '0.25.0';

const BINARIES = [
/(gradlew|\.(jar|keystore|png|jpg|gif))$/,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as React from 'react';

<% if (project.view) { -%>
import { StyleSheet, View } from 'react-native';
import { <%- project.name -%>View } from '<%- project.slug -%>';
<% } else { -%>
<% if (project.arch !== 'new') { -%>
import { useState, useEffect } from 'react';
<% } -%>
import { StyleSheet, View, Text } from 'react-native';
import { multiply } from '<%- project.slug -%>';
<% } -%>
Expand All @@ -28,9 +29,9 @@ export default function App() {
}
<% } else { -%>
export default function App() {
const [result, setResult] = React.useState<number | undefined>();
const [result, setResult] = useState<number | undefined>();

React.useEffect(() => {
useEffect(() => {
multiply(3, 7).then(setResult);
}, []);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@
"name": "<%- project.slug -%>",
"version": "0.1.0",
"description": "<%- project.description %>",
"main": "lib/commonjs/index",
"module": "lib/module/index",
"types": "lib/typescript/src/index.d.ts",
"react-native": "src/index",
"source": "src/index",
"source": "./src/index.tsx",
"main": "./lib/commonjs/index.cjs",
"module": "./lib/module/index.mjs",
"types": "./lib/typescript/src/index.d.ts",
"exports": {
".": {
"types": "./lib/typescript/src/index.d.ts",
"import": "./lib/module/index.mjs",
"require": "./lib/commonjs/index.cjs"
}
},
"files": [
"src",
"lib",
Expand Down Expand Up @@ -130,6 +136,7 @@
"prettier"
],
"rules": {
"react/react-in-jsx-scope": "off",
"prettier/prettier": [
"error",
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
"allowUnusedLabels": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"jsx": "react",
"lib": ["esnext"],
"module": "esnext",
"moduleResolution": "node",
"jsx": "react-jsx",
"lib": ["ESNext"],
"module": "ESNext",
"moduleResolution": "Bundler",
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noImplicitUseStrict": false,
Expand All @@ -22,7 +22,7 @@
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "esnext",
"target": "ESNext",
"verbatimModuleSyntax": true
}
}
19 changes: 17 additions & 2 deletions packages/react-native-builder-bob/babel-preset.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
const browserslist = require('browserslist');

module.exports = function (api, options, cwd) {
const cjs = options.modules === 'commonjs';

return {
presets: [
[
Expand All @@ -24,12 +26,25 @@ module.exports = function (api, options, cwd) {
node: '18',
},
useBuiltIns: false,
modules: options.modules || false,
modules: cjs ? 'commonjs' : false,
},
],
[
require.resolve('@babel/preset-react'),
{
runtime: 'automatic',
},
],
require.resolve('@babel/preset-react'),
require.resolve('@babel/preset-typescript'),
require.resolve('@babel/preset-flow'),
],
plugins: [
[
require.resolve('./lib/babel'),
{
extension: cjs ? 'cjs' : 'mjs',
},
],
],
};
};
101 changes: 79 additions & 22 deletions packages/react-native-builder-bob/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ yargs
const { shouldContinue } = await prompts({
type: 'confirm',
name: 'shouldContinue',
message: `The working directory is not clean. You should commit or stash your changes before configuring bob. Continue anyway?`,
message: `The working directory is not clean.\n You should commit or stash your changes before configuring bob.\n Continue anyway?`,
initial: false,
});

Expand All @@ -41,7 +41,7 @@ yargs

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

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

Expand Down Expand Up @@ -81,7 +81,7 @@ yargs

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

const entries: { [key: string]: string } = {
'main': target
? path.join(output, target, 'index.js')
: path.join(source, entryFile),
'react-native': path.join(source, entryFile),
'source': path.join(source, entryFile),
const entries: {
[key in 'source' | 'main' | 'module' | 'types']?: string;
} = {
source: `./${path.join(source, entryFile)}`,
main: `./${
target
? path.join(output, target, 'index.cjs')
: path.join(source, entryFile)
}`,
};

if (targets.includes('module')) {
entries.module = path.join(output, 'module', 'index.js');
entries.module = `./${path.join(output, 'module', 'index.mjs')}`;
}

if (targets.includes('typescript')) {
entries.types = path.join(output, 'typescript', source, 'index.d.ts');
entries.types = `./${path.join(
output,
'typescript',
source,
'index.d.ts'
)}`;

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

Expand All @@ -180,10 +188,10 @@ yargs
allowUnusedLabels: false,
esModuleInterop: true,
forceConsistentCasingInFileNames: true,
jsx: 'react',
lib: ['esnext'],
module: 'esnext',
moduleResolution: 'node',
jsx: 'react-jsx',
lib: ['ESNext'],
module: 'ESNext',
moduleResolution: 'Bundler',
noFallthroughCasesInSwitch: true,
noImplicitReturns: true,
noImplicitUseStrict: false,
Expand All @@ -194,7 +202,7 @@ yargs
resolveJsonModule: true,
skipLibCheck: true,
strict: true,
target: 'esnext',
target: 'ESNext',
verbatimModuleSyntax: true,
},
},
Expand All @@ -214,13 +222,13 @@ yargs
];

for (const key in entries) {
const entry = entries[key];
const entry = entries[key as keyof typeof entries];

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

Expand All @@ -232,11 +240,60 @@ yargs
}
}

if (Object.values(entries).some((entry) => entry.endsWith('.mjs'))) {
let replace = false;

const exports = {
'.': {
...(entries.types ? { types: entries.types } : null),
...(entries.module ? { import: entries.module } : null),
...(entries.main ? { require: entries.main } : null),
},
};

if (
pkg.exports &&
JSON.stringify(pkg.exports) !== JSON.stringify(exports)
) {
replace = (
await prompts({
type: 'confirm',
name: 'replace',
message: `Your package.json has 'exports' field set.\n Do you want to replace it?`,
initial: true,
})
).replace;
} else {
replace = true;
}

if (replace) {
pkg.exports = exports;
}
}

if (
pkg['react-native'] &&
(pkg['react-native'].startsWith(source) ||
pkg['react-native'].startsWith(`./${source}`))
) {
const { remove } = await prompts({
type: 'confirm',
name: 'remove',
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?`,
initial: true,
});

if (remove) {
delete pkg['react-native'];
}
}

if (pkg.scripts?.prepare && pkg.scripts.prepare !== prepare) {
const { replace } = await prompts({
type: 'confirm',
name: 'replace',
message: `Your package.json has the 'scripts.prepare' field set to '${pkg.scripts.prepare}'. Do you want to replace it with '${prepare}'?`,
message: `Your package.json has the 'scripts.prepare' field set to '${pkg.scripts.prepare}'.\n Do you want to replace it with '${prepare}'?`,
initial: true,
});

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

Expand Down
1 change: 0 additions & 1 deletion packages/react-native-builder-bob/src/targets/commonjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,5 @@ export default async function build({
exclude,
modules: 'commonjs',
report,
field: 'main',
});
}
1 change: 0 additions & 1 deletion packages/react-native-builder-bob/src/targets/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,5 @@ export default async function build({
exclude,
modules: false,
report,
field: 'module',
});
}
Loading

0 comments on commit fb1da66

Please sign in to comment.