fix(react-native): fix buildable react native library (#16749)

This commit is contained in:
Emily Xiong 2023-05-09 12:52:36 -04:00 committed by GitHub
parent c9a7cd8b01
commit 8347e61810
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 165 additions and 122 deletions

View File

@ -1,7 +1,5 @@
# React Native with Nx
![React Logo](/shared/react-logo.png)
Nx provides a holistic dev experience powered by an advanced CLI and editor plugins. It provides rich support for common tools like [Detox](/packages/detox), Storybook, Jest, and more.
In this guide we will show you how to develop [React Native](https://reactnative.dev/) applications with Nx.
@ -80,6 +78,10 @@ happynrwl/
To run the application in development mode:
```shell
npx nx start mobile
```
On Android simulator/device:
```shell
@ -96,7 +98,6 @@ Try out other commands as well.
- `nx lint mobile` to lint the application
- `nx test mobile` to run unit test on the application using Jest
- `nx serve mobile` to serve the application Javascript bundler that communicates with connected devices. This will start the bundler at http://localhost:8081.
- `nx sync-deps mobile` to sync app dependencies to its `package.json`.
### Release build
@ -109,7 +110,9 @@ npx nx build-android mobile
**iOS:** (Mac only)
No CLI support yet. Run in the Xcode project. See: https://reactnative.dev/docs/running-on-device
```shell
npx nx build-ios mobile
```
### E2E
@ -125,7 +128,7 @@ npx nx test-android mobile-e2e
npx nx test-ios mobile-e2e
```
When using React Native in Nx, you get the out-of-the-box support for TypeScript, Detox, and Jest. No need to configure anything: watch mode, source maps, and typings just work.
When using React Native in Nx, you get the out-of-the-box support for TypeScript, Detox, and Jest.
### Adding React Native to an Existing Workspace
@ -258,40 +261,13 @@ dist/libs/shared-ui-layout/
├── lib/
│ └── layout/
│ └── layout.d.ts
├── package.json
├── shared-ui-layout.esm.css
├── shared-ui-layout.esm.js
├── shared-ui-layout.umd.css
└── shared-ui-layout.umd.js
└── package.json
```
This dist folder is ready to be published to a registry.
## Environment Variables
The workspace should install[react-native-config](https://github.com/luggit/react-native-config) by default. To use environment variable, create a new `.env` file in the `happynrwl/apps/mobile` folder:
```
NX_BUILD_NUMBER=123
```
Then access variables defined there from your app:
```javascript
import Config from 'react-native-config';
Config.NX_BUILD_NUMBER; // '123'
```
## Code Sharing
Without Nx, creating a new shared library can take from several hours to even weeks: a new repo needs to be provisioned, CI needs to be set up, etc... In an Nx Workspace, it only takes minutes.
You can share React Native components between multiple React Native applications, share business logic code between React Native mobile applications and plain React web applications. You can even share code between the backend and the frontend. All of these can be done without any unnecessary ceremony.
## Resources
Here are other resources that you may find useful to learn more about React Native and Nx.
- **Blog post:** [Introducing React Native Support for Nx](https://blog.nrwl.io/introducing-react-native-support-for-nx-48d335e90c89) by Jack Hsu
- **Blog post:** [Step by Step Guide on Creating a Monorepo for React Native Apps using Nx](https://blog.nrwl.io/step-by-step-guide-on-creating-a-monorepo-for-react-native-apps-using-nx-704753b6c70e) by Eimly Xiong

View File

@ -7,16 +7,16 @@ import {
Tree,
} from '@nx/devkit';
import { runSymlink } from '../../utils/symlink-task';
import { addLinting } from '../../utils/add-linting';
import { addJest } from '../../utils/add-jest';
import { runSymlink } from '../../utils/symlink-task';
import { normalizeOptions } from './lib/normalize-options';
import initGenerator from '../init/init';
import { addProject } from './lib/add-project';
import { addDetox } from './lib/add-detox';
import { createApplicationFiles } from './lib/create-application-files';
import { addEasScripts } from './lib/add-eas-scripts';
import { addDetox } from './lib/add-detox';
import { Schema } from './schema';
export async function expoApplicationGenerator(
@ -29,20 +29,21 @@ export async function expoApplicationGenerator(
addProject(host, options);
const initTask = await initGenerator(host, { ...options, skipFormat: true });
const lintTask = await addLinting(
host,
options.projectName,
options.appProjectRoot,
[joinPathFragments(options.appProjectRoot, 'tsconfig.app.json')],
options.linter,
options.setParserOptionsProject
);
const lintTask = await addLinting(host, {
...options,
projectRoot: options.appProjectRoot,
tsConfigPaths: [
joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'),
],
});
const jestTask = await addJest(
host,
options.unitTestRunner,
options.projectName,
options.appProjectRoot,
options.js
options.js,
options.skipPackageJson
);
const detoxTask = await addDetox(host, options);
const symlinkTask = runSymlink(host.root, options.appProjectRoot);

View File

@ -1,5 +1,5 @@
{
"extends": "<%= offsetFromRoot %>tsconfig.base.json",
"extends": "<%= rootTsConfigPath %>",
"compilerOptions": {
"jsx": "react-native",
"allowJs": true,

View File

@ -235,7 +235,7 @@ describe('lib', () => {
executor: '@nx/rollup:rollup',
outputs: ['{options.outputPath}'],
options: {
external: ['react/jsx-runtime', 'react-native'],
external: ['react/jsx-runtime', 'react-native', 'react', 'react-dom'],
entryFile: 'libs/my-lib/src/index.ts',
outputPath: 'dist/libs/my-lib',
project: 'libs/my-lib/package.json',

View File

@ -1,6 +1,7 @@
import {
addProjectConfiguration,
convertNxGenerator,
ensurePackage,
formatFiles,
generateFiles,
GeneratorCallback,
@ -15,12 +16,11 @@ import {
updateJson,
} from '@nx/devkit';
import { addTsConfigPath } from '@nx/js';
import { addTsConfigPath, getRelativePathToRootTsConfig } from '@nx/js';
import init from '../init/init';
import { addLinting } from '../../utils/add-linting';
import { addJest } from '../../utils/add-jest';
import { nxVersion } from '../../utils/versions';
import { NormalizedSchema, normalizeOptions } from './lib/normalize-options';
import { Schema } from './schema';
@ -35,23 +35,44 @@ export async function expoLibraryGenerator(
);
}
addProject(host, options);
createFiles(host, options);
const tasks: GeneratorCallback[] = [];
const initTask = await init(host, {
...options,
skipFormat: true,
e2eTestRunner: 'none',
});
tasks.push(initTask);
const lintTask = await addLinting(
const addProjectTask = await addProject(host, options);
if (addProjectTask) {
tasks.push(addProjectTask);
}
createFiles(host, options);
const lintTask = await addLinting(host, {
...options,
projectName: options.name,
tsConfigPaths: [
joinPathFragments(options.projectRoot, 'tsconfig.lib.json'),
],
});
tasks.push(lintTask);
const jestTask = await addJest(
host,
options.unitTestRunner,
options.name,
options.projectRoot,
[joinPathFragments(options.projectRoot, 'tsconfig.lib.json')],
options.linter,
options.setParserOptionsProject
options.js,
options.skipPackageJson
);
tasks.push(jestTask);
if (options.publishable || options.buildable) {
updateLibPackageNpmScope(host, options);
}
if (!options.skipTsConfig) {
addTsConfigPath(host, options.importPath, [
@ -63,31 +84,30 @@ export async function expoLibraryGenerator(
]);
}
const jestTask = await addJest(
host,
options.unitTestRunner,
options.name,
options.projectRoot,
options.js
);
if (options.publishable || options.buildable) {
updateLibPackageNpmScope(host, options);
}
if (!options.skipFormat) {
await formatFiles(host);
}
return runTasksInSerial(initTask, lintTask, jestTask);
return runTasksInSerial(...tasks);
}
function addProject(host: Tree, options: NormalizedSchema) {
async function addProject(host: Tree, options: NormalizedSchema) {
const targets: { [key: string]: TargetConfiguration } = {};
let task: GeneratorCallback;
if (options.publishable || options.buildable) {
const { rollupInitGenerator } = ensurePackage<typeof import('@nx/rollup')>(
'@nx/rollup',
nxVersion
);
const { libsDir } = getWorkspaceLayout(host);
const external = ['react/jsx-runtime', 'react-native'];
const external = [
'react/jsx-runtime',
'react-native',
'react',
'react-dom',
];
targets.build = {
executor: '@nx/rollup:rollup',
@ -108,6 +128,7 @@ function addProject(host: Tree, options: NormalizedSchema) {
],
},
};
task = await rollupInitGenerator(host, { ...options, skipFormat: true });
}
addProjectConfiguration(host, options.name, {
@ -117,6 +138,8 @@ function addProject(host: Tree, options: NormalizedSchema) {
tags: options.parsedTags,
targets,
});
return task;
}
function updateTsConfig(tree: Tree, options: NormalizedSchema) {
@ -149,6 +172,10 @@ function createFiles(host: Tree, options: NormalizedSchema) {
...names(options.name),
tmpl: '',
offsetFromRoot: offsetFromRoot(options.projectRoot),
rootTsConfigPath: getRelativePathToRootTsConfig(
host,
options.projectRoot
),
}
);

View File

@ -6,18 +6,21 @@ export async function addJest(
unitTestRunner: 'jest' | 'none',
projectName: string,
appProjectRoot: string,
js: boolean
js: boolean,
skipPackageJson: boolean
) {
if (unitTestRunner !== 'jest') {
return () => {};
}
const jestTask = await jestProjectGenerator(host, {
js,
project: projectName,
supportTsx: true,
skipSerializers: true,
setupFile: 'none',
babelJest: true,
compiler: 'babel',
skipPackageJson,
skipFormat: true,
});

View File

@ -15,14 +15,13 @@ describe('Add Linting', () => {
});
});
it('should add update `project configuration` file properly when eslint is passed', () => {
addLinting(
tree,
'my-lib',
'libs/my-lib',
['libs/my-lib/tsconfig.lib.json'],
Linter.EsLint
);
it('should add update configuration when eslint is passed', () => {
addLinting(tree, {
projectName: 'my-lib',
linter: Linter.EsLint,
tsConfigPaths: ['libs/my-lib/tsconfig.lib.json'],
projectRoot: 'libs/my-lib',
});
const project = readProjectConfiguration(tree, 'my-lib');
expect(project.targets.lint).toBeDefined();
@ -30,13 +29,12 @@ describe('Add Linting', () => {
});
it('should not add lint target when "none" is passed', async () => {
addLinting(
tree,
'my-lib',
'libs/my-lib',
['libs/my-lib/tsconfig.lib.json'],
Linter.None
);
addLinting(tree, {
projectName: 'my-lib',
linter: Linter.None,
tsConfigPaths: ['libs/my-lib/tsconfig.lib.json'],
projectRoot: 'libs/my-lib',
});
const project = readProjectConfiguration(tree, 'my-lib');
expect(project.targets.lint).toBeUndefined();

View File

@ -1,38 +1,47 @@
import { Linter, lintProjectGenerator } from '@nx/linter';
import {
addDependenciesToPackageJson,
GeneratorCallback,
joinPathFragments,
runTasksInSerial,
Tree,
updateJson,
} from '@nx/devkit';
import { extendReactEslintJson, extraEslintDependencies } from '@nx/react';
import {
extendReactEslintJson,
extraEslintDependencies,
} from '@nx/react/src/utils/lint';
import type { Linter as ESLintLinter } from 'eslint';
export async function addLinting(
host: Tree,
projectName: string,
appProjectRoot: string,
tsConfigPaths: string[],
linter: Linter,
setParserOptionsProject?: boolean
) {
if (linter === Linter.None) {
interface NormalizedSchema {
linter?: Linter;
projectName: string;
projectRoot: string;
setParserOptionsProject?: boolean;
tsConfigPaths: string[];
skipPackageJson?: boolean;
}
export async function addLinting(host: Tree, options: NormalizedSchema) {
if (options.linter === Linter.None) {
return () => {};
}
const tasks: GeneratorCallback[] = [];
const lintTask = await lintProjectGenerator(host, {
linter,
project: projectName,
tsConfigPaths,
eslintFilePatterns: [`${appProjectRoot}/**/*.{ts,tsx,js,jsx}`],
linter: options.linter,
project: options.projectName,
tsConfigPaths: options.tsConfigPaths,
eslintFilePatterns: [`${options.projectRoot}/**/*.{ts,tsx,js,jsx}`],
skipFormat: true,
setParserOptionsProject,
skipPackageJson: options.skipPackageJson,
});
tasks.push(lintTask);
updateJson(
host,
joinPathFragments(appProjectRoot, '.eslintrc.json'),
joinPathFragments(options.projectRoot, '.eslintrc.json'),
(json: ESLintLinter.Config) => {
json = extendReactEslintJson(json);
@ -49,11 +58,14 @@ export async function addLinting(
}
);
if (!options.skipPackageJson) {
const installTask = await addDependenciesToPackageJson(
host,
extraEslintDependencies.dependencies,
extraEslintDependencies.devDependencies
);
tasks.push(installTask);
}
return runTasksInSerial(lintTask, installTask);
return runTasksInSerial(...tasks);
}

View File

@ -1,5 +1,3 @@
import { join } from 'path';
import {
convertNxGenerator,
formatFiles,
@ -51,11 +49,11 @@ export async function reactNativeApplicationGenerator(
const detoxTask = await addDetox(host, options);
const symlinkTask = runSymlink(host.root, options.appProjectRoot);
const podInstallTask = runPodInstall(
join(host.root, options.iosProjectRoot),
joinPathFragments(host.root, options.iosProjectRoot),
options.install
);
const chmodTaskGradlew = chmodAndroidGradlewFilesTask(
join(host.root, options.androidProjectRoot)
joinPathFragments(host.root, options.androidProjectRoot)
);
if (!options.skipFormat) {

View File

@ -1,5 +1,4 @@
import { getWorkspaceLayout, joinPathFragments, names, Tree } from '@nx/devkit';
import { join } from 'path';
import { Schema } from '../schema';
export interface NormalizedSchema extends Schema {
@ -30,8 +29,8 @@ export function normalizeOptions(
const appProjectName = projectDirectory.replace(/\//g, '-');
const appProjectRoot = joinPathFragments(appsDir, projectDirectory);
const iosProjectRoot = join(appProjectRoot, 'ios');
const androidProjectRoot = join(appProjectRoot, 'android');
const iosProjectRoot = joinPathFragments(appProjectRoot, 'ios');
const androidProjectRoot = joinPathFragments(appProjectRoot, 'android');
const parsedTags = options.tags
? options.tags.split(',').map((s) => s.trim())

View File

@ -271,7 +271,7 @@ describe('lib', () => {
executor: '@nx/rollup:rollup',
outputs: ['{options.outputPath}'],
options: {
external: ['react/jsx-runtime', 'react-native'],
external: ['react/jsx-runtime', 'react-native', 'react', 'react-dom'],
entryFile: 'libs/my-lib/src/index.ts',
outputPath: 'dist/libs/my-lib',
project: 'libs/my-lib/package.json',

View File

@ -1,6 +1,7 @@
import {
addProjectConfiguration,
convertNxGenerator,
ensurePackage,
formatFiles,
generateFiles,
GeneratorCallback,
@ -19,6 +20,7 @@ import { addTsConfigPath, getRelativePathToRootTsConfig } from '@nx/js';
import init from '../init/init';
import { addLinting } from '../../utils/add-linting';
import { addJest } from '../../utils/add-jest';
import { nxVersion } from '../../utils/versions';
import { NormalizedSchema, normalizeOptions } from './lib/normalize-options';
import { Schema } from './schema';
@ -33,13 +35,20 @@ export async function reactNativeLibraryGenerator(
);
}
const tasks: GeneratorCallback[] = [];
const initTask = await init(host, {
...options,
skipFormat: true,
e2eTestRunner: 'none',
});
tasks.push(initTask);
const addProjectTask = await addProject(host, options);
if (addProjectTask) {
tasks.push(addProjectTask);
}
addProject(host, options);
createFiles(host, options);
const lintTask = await addLinting(host, {
@ -49,6 +58,7 @@ export async function reactNativeLibraryGenerator(
joinPathFragments(options.projectRoot, 'tsconfig.lib.json'),
],
});
tasks.push(lintTask);
const jestTask = await addJest(
host,
@ -58,6 +68,7 @@ export async function reactNativeLibraryGenerator(
options.js,
options.skipPackageJson
);
tasks.push(jestTask);
if (options.publishable || options.buildable) {
updateLibPackageNpmScope(host, options);
@ -65,7 +76,11 @@ export async function reactNativeLibraryGenerator(
if (!options.skipTsConfig) {
addTsConfigPath(host, options.importPath, [
joinPathFragments(options.projectRoot, './src', 'index.ts'),
joinPathFragments(
options.projectRoot,
'./src',
'index.' + (options.js ? 'js' : 'ts')
),
]);
}
@ -73,15 +88,26 @@ export async function reactNativeLibraryGenerator(
await formatFiles(host);
}
return runTasksInSerial(initTask, lintTask, jestTask);
return runTasksInSerial(...tasks);
}
function addProject(host: Tree, options: NormalizedSchema) {
async function addProject(host: Tree, options: NormalizedSchema) {
const targets: { [key: string]: TargetConfiguration } = {};
let task: GeneratorCallback;
if (options.publishable || options.buildable) {
const { rollupInitGenerator } = ensurePackage<typeof import('@nx/rollup')>(
'@nx/rollup',
nxVersion
);
const { libsDir } = getWorkspaceLayout(host);
const external = ['react/jsx-runtime', 'react-native'];
const external = [
'react/jsx-runtime',
'react-native',
'react',
'react-dom',
];
targets.build = {
executor: '@nx/rollup:rollup',
@ -102,6 +128,7 @@ function addProject(host: Tree, options: NormalizedSchema) {
],
},
};
task = await rollupInitGenerator(host, { ...options, skipFormat: true });
}
addProjectConfiguration(host, options.name, {
@ -111,6 +138,8 @@ function addProject(host: Tree, options: NormalizedSchema) {
tags: options.parsedTags,
targets,
});
return task;
}
function updateTsConfig(tree: Tree, options: NormalizedSchema) {