Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ESM support for generated project #583

Merged
merged 3 commits into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading