From 8347e618101f2d2fd0880d366a10e2147f8f68e4 Mon Sep 17 00:00:00 2001 From: Emily Xiong Date: Tue, 9 May 2023 12:52:36 -0400 Subject: [PATCH] fix(react-native): fix buildable react native library (#16749) --- docs/shared/guides/react-native.md | 42 +++-------- .../src/generators/application/application.ts | 23 +++--- .../library/files/lib/tsconfig.json.template | 2 +- .../src/generators/library/library.spec.ts | 2 +- .../expo/src/generators/library/library.ts | 75 +++++++++++++------ packages/expo/src/utils/add-jest.ts | 7 +- packages/expo/src/utils/add-linting.spec.ts | 28 ++++--- packages/expo/src/utils/add-linting.ts | 56 ++++++++------ .../src/generators/application/application.ts | 6 +- .../application/lib/normalize-options.ts | 5 +- .../src/generators/library/library.spec.ts | 2 +- .../src/generators/library/library.ts | 39 ++++++++-- 12 files changed, 165 insertions(+), 122 deletions(-) diff --git a/docs/shared/guides/react-native.md b/docs/shared/guides/react-native.md index 45f1f1478c5a8..c78b2f4d6edb0 100644 --- a/docs/shared/guides/react-native.md +++ b/docs/shared/guides/react-native.md @@ -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 diff --git a/packages/expo/src/generators/application/application.ts b/packages/expo/src/generators/application/application.ts index 741e0907f6f76..627feb7869763 100644 --- a/packages/expo/src/generators/application/application.ts +++ b/packages/expo/src/generators/application/application.ts @@ -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); diff --git a/packages/expo/src/generators/library/files/lib/tsconfig.json.template b/packages/expo/src/generators/library/files/lib/tsconfig.json.template index 441ee25621bd7..dacf3c7f5e0ac 100644 --- a/packages/expo/src/generators/library/files/lib/tsconfig.json.template +++ b/packages/expo/src/generators/library/files/lib/tsconfig.json.template @@ -1,5 +1,5 @@ { - "extends": "<%= offsetFromRoot %>tsconfig.base.json", + "extends": "<%= rootTsConfigPath %>", "compilerOptions": { "jsx": "react-native", "allowJs": true, diff --git a/packages/expo/src/generators/library/library.spec.ts b/packages/expo/src/generators/library/library.spec.ts index 6a23737230efe..2105efe5fc9bf 100644 --- a/packages/expo/src/generators/library/library.spec.ts +++ b/packages/expo/src/generators/library/library.spec.ts @@ -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', diff --git a/packages/expo/src/generators/library/library.ts b/packages/expo/src/generators/library/library.ts index 2c0017f9536e2..6145b2cdb1d7c 100644 --- a/packages/expo/src/generators/library/library.ts +++ b/packages/expo/src/generators/library/library.ts @@ -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 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 lintTask = await addLinting( + 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( + '@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 + ), } ); diff --git a/packages/expo/src/utils/add-jest.ts b/packages/expo/src/utils/add-jest.ts index 848d280e04227..5e225f5f6a776 100644 --- a/packages/expo/src/utils/add-jest.ts +++ b/packages/expo/src/utils/add-jest.ts @@ -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, }); diff --git a/packages/expo/src/utils/add-linting.spec.ts b/packages/expo/src/utils/add-linting.spec.ts index 3256aae1ab3af..f233d7bf1307f 100644 --- a/packages/expo/src/utils/add-linting.spec.ts +++ b/packages/expo/src/utils/add-linting.spec.ts @@ -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(); diff --git a/packages/expo/src/utils/add-linting.ts b/packages/expo/src/utils/add-linting.ts index 18bcdabacbeee..438d91f183be6 100644 --- a/packages/expo/src/utils/add-linting.ts +++ b/packages/expo/src/utils/add-linting.ts @@ -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( } ); - const installTask = await addDependenciesToPackageJson( - host, - extraEslintDependencies.dependencies, - extraEslintDependencies.devDependencies - ); + if (!options.skipPackageJson) { + const installTask = await addDependenciesToPackageJson( + host, + extraEslintDependencies.dependencies, + extraEslintDependencies.devDependencies + ); + tasks.push(installTask); + } - return runTasksInSerial(lintTask, installTask); + return runTasksInSerial(...tasks); } diff --git a/packages/react-native/src/generators/application/application.ts b/packages/react-native/src/generators/application/application.ts index 675316c061f49..2381112bae027 100644 --- a/packages/react-native/src/generators/application/application.ts +++ b/packages/react-native/src/generators/application/application.ts @@ -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) { diff --git a/packages/react-native/src/generators/application/lib/normalize-options.ts b/packages/react-native/src/generators/application/lib/normalize-options.ts index 6df4942fa79f0..b370b36b86de7 100644 --- a/packages/react-native/src/generators/application/lib/normalize-options.ts +++ b/packages/react-native/src/generators/application/lib/normalize-options.ts @@ -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()) diff --git a/packages/react-native/src/generators/library/library.spec.ts b/packages/react-native/src/generators/library/library.spec.ts index 165bd00bc01ab..4851ef14ad040 100644 --- a/packages/react-native/src/generators/library/library.spec.ts +++ b/packages/react-native/src/generators/library/library.spec.ts @@ -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', diff --git a/packages/react-native/src/generators/library/library.ts b/packages/react-native/src/generators/library/library.ts index 9feea8af7617c..eda3d9d27de50 100644 --- a/packages/react-native/src/generators/library/library.ts +++ b/packages/react-native/src/generators/library/library.ts @@ -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( + '@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) {