diff --git a/docs/commands.md b/docs/commands.md index 4516390c9..fa40b1866 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -305,6 +305,8 @@ Builds your app and starts it on a connected Android emulator or device. #### `--root [string]` +> **DEPRECATED** – root is discovered automatically + Override the root directory for the Android build (which contains the android directory)'. #### `--variant [string]` @@ -315,12 +317,16 @@ Specify your app's build variant. #### `--appFolder [string]` +> **DEPRECATED** – use "platforms.android.appName" in react-native.config.js + > default: 'app' Specify a different application folder name for the Android source. If not, we assume is "app". #### `--appId [string]` +> **DEPRECATED** – use "platforms.android.appName" in react-native.config.js + Specify an `applicationId` to launch after build. #### `--appIdSuffix [string]` diff --git a/docs/dependencies.md b/docs/dependencies.md index 4bd2d35f6..c480f07a3 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -91,7 +91,7 @@ An array of iOS script phases to add to the project. Specifying a `path` propert module.exports = { dependency: { platforms: { - ios: { + ios: { scriptPhases: [ { name: '[MY DEPENDENCY] My Script', @@ -109,7 +109,11 @@ See [`script_phase` options](https://www.rubydoc.info/gems/cocoapods-core/Pod/Po #### platforms.android.sourceDir -A relative path to a folder with source files. E.g. `custom-android`, or `custom-android/app`. By default, CLI searches for `android` and `android/app` as source dirs. +A relative path to a folder with Android project (Gradle root project), e.g. `./path/to/custom-android`. By default, CLI searches for `./android` as source dir. + +#### platforms.android.appName + +A name of the app in the Android `sourceDir`, equivalent to Gradle project name. By default it's `app`. #### platforms.android.manifestPath diff --git a/packages/cli-types/src/android.ts b/packages/cli-types/src/android.ts index 54fd885b7..a871b0961 100644 --- a/packages/cli-types/src/android.ts +++ b/packages/cli-types/src/android.ts @@ -9,31 +9,20 @@ export interface AndroidProjectConfig { assetsPath: string; mainFilePath: string; packageName: string; + packageFolder: string; + appName: string; } -export interface AndroidProjectParams { - sourceDir?: string; - manifestPath?: string; - packageName?: string; - packageFolder?: string; - mainFilePath?: string; - stringsPath?: string; - settingsGradlePath?: string; - assetsPath?: string; - buildGradlePath?: string; -} +export type AndroidProjectParams = Partial; export interface AndroidDependencyConfig { sourceDir: string; folder: string; packageImportPath: string; packageInstance: string; + appName: string; + manifestPath: string; + packageName: string; } -export interface AndroidDependencyParams { - packageName?: string; - sourceDir?: string; - manifestPath?: string; - packageImportPath?: string; - packageInstance?: string; -} +export type AndroidDependencyParams = Partial; diff --git a/packages/cli/src/tools/config/schema.ts b/packages/cli/src/tools/config/schema.ts index 79f53f36b..bae86851c 100644 --- a/packages/cli/src/tools/config/schema.ts +++ b/packages/cli/src/tools/config/schema.ts @@ -59,6 +59,7 @@ export const dependencyConfig = t manifestPath: t.string(), packageImportPath: t.string(), packageInstance: t.string(), + appName: t.string(), }) .default({}), }) @@ -126,6 +127,7 @@ export const projectConfig = t folder: t.string(), packageImportPath: t.string(), packageInstance: t.string(), + appName: t.string(), }) .allow(null), }), diff --git a/packages/platform-android/src/commands/runAndroid/__tests__/runOnAllDevices.test.ts b/packages/platform-android/src/commands/runAndroid/__tests__/runOnAllDevices.test.ts index 830b2c56a..540861c06 100644 --- a/packages/platform-android/src/commands/runAndroid/__tests__/runOnAllDevices.test.ts +++ b/packages/platform-android/src/commands/runAndroid/__tests__/runOnAllDevices.test.ts @@ -7,83 +7,169 @@ */ import runOnAllDevices from '../runOnAllDevices'; +import execa from 'execa'; -jest.mock('child_process', () => ({ - execFileSync: jest.fn(), - spawnSync: jest.fn(), -})); - +jest.mock('execa'); jest.mock('../getAdbPath'); jest.mock('../tryLaunchEmulator'); -const {execFileSync} = require('child_process'); describe('--appFolder', () => { + const args = { + root: '/root', + appFolder: undefined, + appId: '', + tasks: undefined, + variant: 'debug', + appIdSuffix: '', + mainActivity: 'MainActivity', + deviceId: undefined, + packager: true, + port: 8081, + terminal: 'iTerm2', + jetifier: true, + }; + const androidProject = { + manifestPath: '/android/app/src/main/AndroidManifest.xml', + appName: 'app', + packageName: 'com.test', + sourceDir: '/android', + isFlat: false, + folder: '', + stringsPath: '', + buildGradlePath: '', + settingsGradlePath: '', + assetsPath: '', + mainFilePath: '', + packageFolder: '', + }; beforeEach(() => { jest.clearAllMocks(); }); it('uses task "install[Variant]" as default task', async () => { - // @ts-ignore - await runOnAllDevices({ - variant: 'debug', - }); - expect(execFileSync.mock.calls[0][1]).toContain('installDebug'); + await runOnAllDevices( + {...args, variant: 'debug'}, + './gradlew', + 'com.testapp', + 'adb', + androidProject, + ); + expect(((execa as unknown) as jest.Mock).mock.calls[0][1]).toContain( + 'app:installDebug', + ); + }); + + it('uses appName and default variant', async () => { + await runOnAllDevices( + {...args, variant: 'debug'}, + './gradlew', + 'com.testapp', + 'adb', + {...androidProject, appName: 'someApp'}, + ); + + expect(((execa as unknown) as jest.Mock).mock.calls[0][1]).toContain( + 'someApp:installDebug', + ); + }); + + it('uses appName and custom variant', async () => { + await runOnAllDevices( + {...args, variant: 'staging'}, + './gradlew', + 'com.testapp', + 'adb', + {...androidProject, appName: 'anotherApp'}, + ); + + expect(((execa as unknown) as jest.Mock).mock.calls[0][1]).toContain( + 'anotherApp:installStaging', + ); }); it('uses appFolder and default variant', async () => { - // @ts-ignore - await runOnAllDevices({ - appFolder: 'someApp', - variant: 'debug', - }); + await runOnAllDevices( + {...args, appFolder: 'someApp', variant: 'debug'}, + './gradlew', + 'com.testapp', + 'adb', + androidProject, + ); - expect(execFileSync.mock.calls[0][1]).toContain('someApp:installDebug'); + expect(((execa as unknown) as jest.Mock).mock.calls[0][1]).toContain( + 'someApp:installDebug', + ); }); it('uses appFolder and custom variant', async () => { - // @ts-ignore - await runOnAllDevices({ - appFolder: 'anotherApp', - variant: 'staging', - }); + await runOnAllDevices( + {...args, appFolder: 'anotherApp', variant: 'staging'}, + './gradlew', + 'com.testapp', + 'adb', + androidProject, + ); - expect(execFileSync.mock.calls[0][1]).toContain( + expect(((execa as unknown) as jest.Mock).mock.calls[0][1]).toContain( 'anotherApp:installStaging', ); }); it('uses only task argument', async () => { - // @ts-ignore - await runOnAllDevices({ - tasks: ['someTask'], - variant: 'debug', - }); + await runOnAllDevices( + {...args, tasks: ['someTask']}, + './gradlew', + 'com.testapp', + 'adb', + androidProject, + ); - expect(execFileSync.mock.calls[0][1]).toContain('someTask'); + expect(((execa as unknown) as jest.Mock).mock.calls[0][1]).toContain( + 'app:someTask', + ); + }); + + it('uses appName and custom task argument', async () => { + await runOnAllDevices( + {...args, tasks: ['someTask']}, + './gradlew', + 'com.testapp', + 'adb', + {...androidProject, appName: 'anotherApp'}, + ); + + expect(((execa as unknown) as jest.Mock).mock.calls[0][1]).toContain( + 'anotherApp:someTask', + ); }); it('uses appFolder and custom task argument', async () => { - // @ts-ignore - await runOnAllDevices({ - appFolder: 'anotherApp', - tasks: ['someTask'], - variant: 'debug', - }); - - expect(execFileSync.mock.calls[0][1]).toContain('anotherApp:someTask'); + await runOnAllDevices( + {...args, appFolder: 'anotherApp', tasks: ['someTask'], variant: 'debug'}, + './gradlew', + 'com.testapp', + 'adb', + {...androidProject, appName: 'anotherApp'}, + ); + + expect(((execa as unknown) as jest.Mock).mock.calls[0][1]).toContain( + 'anotherApp:someTask', + ); }); it('uses multiple tasks', async () => { - // @ts-ignore - await runOnAllDevices({ - appFolder: 'app', - tasks: ['clean', 'someTask'], - }); + await runOnAllDevices( + {...args, tasks: ['clean', 'someTask']}, + './gradlew', + 'com.testapp', + 'adb', + androidProject, + ); - expect(execFileSync.mock.calls[0][1]).toContain( + expect(((execa as unknown) as jest.Mock).mock.calls[0][1]).toEqual([ 'app:clean', - // @ts-ignore 'app:someTask', - ); + '-PreactNativeDevServerPort=8081', + ]); }); }); diff --git a/packages/platform-android/src/commands/runAndroid/index.ts b/packages/platform-android/src/commands/runAndroid/index.ts index c9375608d..73334e8c0 100644 --- a/packages/platform-android/src/commands/runAndroid/index.ts +++ b/packages/platform-android/src/commands/runAndroid/index.ts @@ -23,25 +23,28 @@ import { } from '@react-native-community/cli-tools'; import warnAboutManuallyLinkedLibs from '../../link/warnAboutManuallyLinkedLibs'; -// Verifies this is an Android project -function checkAndroid(root: string) { - return fs.existsSync(path.join(root, 'android/gradlew')); -} - // Validates that the package name is correct function validatePackageName(packageName: string) { return /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/.test(packageName); } -function performChecks(config: Config, args: Flags) { - if (!checkAndroid(args.root)) { - throw new CLIError( - 'Android project not found. Are you sure this is a React Native project?', +function displayWarnings(config: Config, args: Flags) { + warnAboutManuallyLinkedLibs(config); + if (args.appId) { + logger.warn( + 'Using deprecated "--appId" flag. Use "platforms.android.appName" in react-native.config.js instead.', + ); + } + if (args.appFolder) { + logger.warn( + 'Using deprecated "--appFolder" flag. Use "platforms.android.appName" in react-native.config.js instead.', + ); + } + if (args.root) { + logger.warn( + 'Using deprecated "--root" flag. App root is discovered automatically.', ); } - - // warn after we have done basic system checks - warnAboutManuallyLinkedLibs(config); } export interface Flags { @@ -59,11 +62,22 @@ export interface Flags { jetifier: boolean; } +type AndroidProject = NonNullable; + /** * Starts the app on a connected Android emulator or device. */ async function runAndroid(_argv: Array, config: Config, args: Flags) { - performChecks(config, args); + displayWarnings(config, args); + const androidProject = config.project.android; + + if (!androidProject) { + throw new CLIError(` + Android project not found. Are you sure this is a React Native project? + If your Android files are located in a non-standard location (e.g. not inside \'android\' folder), consider setting + \`project.android.sourceDir\` option to point to a new location. +`); + } if (args.jetifier) { logger.info( @@ -82,7 +96,7 @@ async function runAndroid(_argv: Array, config: Config, args: Flags) { } if (!args.packager) { - return buildAndRun(args); + return buildAndRun(args, androidProject); } return isPackagerRunning(args.port).then((result: string) => { @@ -107,35 +121,21 @@ async function runAndroid(_argv: Array, config: Config, args: Flags) { ); } } - return buildAndRun(args); + return buildAndRun(args, androidProject); }); } -function getPackageNameWithSuffix( - appId: string, - appIdSuffix: string, - packageName: string, -) { - if (appId) { - return appId; - } - if (appIdSuffix) { - return `${packageName}.${appIdSuffix}`; - } - - return packageName; -} - // Builds the app and runs it on a connected emulator / device. -function buildAndRun(args: Flags) { +function buildAndRun(args: Flags, androidProject: AndroidProject) { process.chdir(path.join(args.root, 'android')); const cmd = process.platform.startsWith('win') ? 'gradlew.bat' : './gradlew'; // "app" is usually the default value for Android apps with only 1 app + const {appName} = androidProject; const {appFolder} = args; // @ts-ignore const androidManifest = fs.readFileSync( - `${appFolder}/src/main/AndroidManifest.xml`, + `${appFolder || appName}/src/main/AndroidManifest.xml`, 'utf8', ); @@ -143,7 +143,7 @@ function buildAndRun(args: Flags) { if (!packageNameMatchArray || packageNameMatchArray.length === 0) { throw new CLIError( `Failed to build the app: No package name found. Found errors in ${chalk.underline.dim( - `${appFolder}/src/main/AndroidManifest.xml`, + `${appFolder || appName}/src/main/AndroidManifest.xml`, )}`, ); } @@ -160,49 +160,32 @@ function buildAndRun(args: Flags) { ); // we can also directly add the package naming rules here } - const packageNameWithSuffix = getPackageNameWithSuffix( - args.appId, - args.appIdSuffix, - packageName, - ); const adbPath = getAdbPath(); if (args.deviceId) { - return runOnSpecificDevice( - args, - cmd, - packageNameWithSuffix, - packageName, - adbPath, - ); + return runOnSpecificDevice(args, cmd, packageName, adbPath, androidProject); } else { - return runOnAllDevices( - args, - cmd, - packageNameWithSuffix, - packageName, - adbPath, - ); + return runOnAllDevices(args, cmd, packageName, adbPath, androidProject); } } function runOnSpecificDevice( args: Flags, gradlew: 'gradlew.bat' | './gradlew', - packageNameWithSuffix: string, packageName: string, adbPath: string, + androidProject: AndroidProject, ) { const devices = adb.getDevices(adbPath); const {deviceId} = args; if (devices.length > 0 && deviceId) { if (devices.indexOf(deviceId) !== -1) { - buildApk(gradlew); + buildApk(gradlew, androidProject.sourceDir); installAndLaunchOnDevice( args, deviceId, - packageNameWithSuffix, packageName, adbPath, + androidProject, ); } else { logger.error( @@ -215,26 +198,32 @@ function runOnSpecificDevice( } } -function buildApk(gradlew: string) { +function buildApk(gradlew: string, sourceDir: string) { try { // using '-x lint' in order to ignore linting errors while building the apk const gradleArgs = ['build', '-x', 'lint']; logger.info('Building the app...'); logger.debug(`Running command "${gradlew} ${gradleArgs.join(' ')}"`); - execa.sync(gradlew, gradleArgs, {stdio: 'inherit'}); + execa.sync(gradlew, gradleArgs, {stdio: 'inherit', cwd: sourceDir}); } catch (error) { throw new CLIError('Failed to build the app.', error); } } -function tryInstallAppOnDevice(args: Flags, adbPath: string, device: string) { +function tryInstallAppOnDevice( + args: Flags, + adbPath: string, + device: string, + androidProject: AndroidProject, +) { try { // "app" is usually the default value for Android apps with only 1 app + const {appName, sourceDir} = androidProject; const {appFolder} = args; const variant = args.variant.toLowerCase(); - const buildDirectory = `${appFolder}/build/outputs/apk/${variant}`; + const buildDirectory = `${sourceDir}/${appName}/build/outputs/apk/${variant}`; const apkFile = getInstallApkName( - appFolder, + appFolder || appName, // TODO: remove appFolder adbPath, variant, device, @@ -254,7 +243,7 @@ function tryInstallAppOnDevice(args: Flags, adbPath: string, device: string) { } function getInstallApkName( - appFolder: string, + appName: string, adbPath: string, variant: string, device: string, @@ -264,14 +253,14 @@ function getInstallApkName( // check if there is an apk file like app-armeabi-v7a-debug.apk for (const availableCPU of availableCPUs.concat('universal')) { - const apkName = `${appFolder}-${availableCPU}-${variant}.apk`; + const apkName = `${appName}-${availableCPU}-${variant}.apk`; if (fs.existsSync(`${buildDirectory}/${apkName}`)) { return apkName; } } // check if there is a default file like app-debug.apk - const apkName = `${appFolder}-${variant}.apk`; + const apkName = `${appName}-${variant}.apk`; if (fs.existsSync(`${buildDirectory}/${apkName}`)) { return apkName; } @@ -282,22 +271,15 @@ function getInstallApkName( function installAndLaunchOnDevice( args: Flags, selectedDevice: string, - packageNameWithSuffix: string, packageName: string, adbPath: string, + androidProject: AndroidProject, ) { tryRunAdbReverse(args.port, selectedDevice); - tryInstallAppOnDevice(args, adbPath, selectedDevice); - tryLaunchAppOnDevice( - selectedDevice, - packageNameWithSuffix, - packageName, - adbPath, - args.mainActivity, - ); + tryInstallAppOnDevice(args, adbPath, selectedDevice, androidProject); + tryLaunchAppOnDevice(selectedDevice, packageName, adbPath, args); } -// @ts-ignore function startServerInNewWindow( port: number, terminal: string, @@ -396,6 +378,7 @@ function startServerInNewWindow( logger.error( `Cannot start the packager. Unknown platform ${process.platform}`, ); + return; } export default { @@ -407,7 +390,7 @@ export default { { name: '--root [string]', description: - 'Override the root directory for the android build (which contains the android directory)', + '[DEPRECATED - root is discovered automatically] Override the root directory for the android build (which contains the android directory)', default: '', }, { @@ -418,12 +401,12 @@ export default { { name: '--appFolder [string]', description: - 'Specify a different application folder name for the android source. If not, we assume is "app"', - default: 'app', + '[DEPRECATED – use "platforms.android.appName" in react-native.config.js] Specify a different application folder name for the android source. If not, we assume is "app"', }, { name: '--appId [string]', - description: 'Specify an applicationId to launch after build.', + description: + '[DEPRECATED – use "platforms.android.appName" in react-native.config.js] Specify an applicationId to launch after build.', default: '', }, { diff --git a/packages/platform-android/src/commands/runAndroid/runOnAllDevices.ts b/packages/platform-android/src/commands/runAndroid/runOnAllDevices.ts index 81c734fea..ddc78a4c4 100644 --- a/packages/platform-android/src/commands/runAndroid/runOnAllDevices.ts +++ b/packages/platform-android/src/commands/runAndroid/runOnAllDevices.ts @@ -7,7 +7,8 @@ */ import chalk from 'chalk'; -import {execFileSync} from 'child_process'; +import execa from 'execa'; +import {Config} from '@react-native-community/cli-types'; import {logger, CLIError} from '@react-native-community/cli-tools'; import adb from './adb'; import tryRunAdbReverse from './tryRunAdbReverse'; @@ -15,25 +16,22 @@ import tryLaunchAppOnDevice from './tryLaunchAppOnDevice'; import tryLaunchEmulator from './tryLaunchEmulator'; import {Flags} from '.'; -function getTaskNames( - appFolder: string, - commands: Array, -): Array { - return appFolder - ? commands.map(command => `${appFolder}:${command}`) - : commands; +function getTaskNames(appName: string, commands: Array): Array { + return appName ? commands.map(command => `${appName}:${command}`) : commands; } function toPascalCase(value: string) { - return value[0].toUpperCase() + value.slice(1); + return value !== '' ? value[0].toUpperCase() + value.slice(1) : value; } +type AndroidProject = NonNullable; + async function runOnAllDevices( args: Flags, cmd: string, - packageNameWithSuffix: string, packageName: string, adbPath: string, + androidProject: AndroidProject, ) { let devices = adb.getDevices(adbPath); if (devices.length === 0) { @@ -53,8 +51,13 @@ async function runOnAllDevices( } try { - const tasks = args.tasks || ['install' + toPascalCase(args.variant)]; - const gradleArgs = getTaskNames(args.appFolder, tasks); + const tasks = args.tasks || [ + 'install' + toPascalCase(args.appIdSuffix) + toPascalCase(args.variant), + ]; + const gradleArgs = getTaskNames( + args.appFolder || androidProject.appName, + tasks, + ); if (args.port != null) { gradleArgs.push('-PreactNativeDevServerPort=' + args.port); @@ -65,7 +68,10 @@ async function runOnAllDevices( `Running command "cd android && ${cmd} ${gradleArgs.join(' ')}"`, ); - execFileSync(cmd, gradleArgs, {stdio: ['inherit', 'inherit', 'pipe']}); + await execa(cmd, gradleArgs, { + stdio: ['inherit', 'inherit', 'pipe'], + cwd: androidProject.sourceDir, + }); } catch (error) { throw createInstallError(error); } @@ -73,13 +79,7 @@ async function runOnAllDevices( (devices.length > 0 ? devices : [undefined]).forEach( (device: string | void) => { tryRunAdbReverse(args.port, device); - tryLaunchAppOnDevice( - device, - packageNameWithSuffix, - packageName, - adbPath, - args.mainActivity, - ); + tryLaunchAppOnDevice(device, packageName, adbPath, args); }, ); } diff --git a/packages/platform-android/src/commands/runAndroid/tryLaunchAppOnDevice.ts b/packages/platform-android/src/commands/runAndroid/tryLaunchAppOnDevice.ts index 10e4ab74e..edf0bbe2b 100644 --- a/packages/platform-android/src/commands/runAndroid/tryLaunchAppOnDevice.ts +++ b/packages/platform-android/src/commands/runAndroid/tryLaunchAppOnDevice.ts @@ -6,23 +6,26 @@ * */ -import {spawnSync} from 'child_process'; +import execa from 'execa'; +import {Flags} from '.'; import {logger, CLIError} from '@react-native-community/cli-tools'; function tryLaunchAppOnDevice( device: string | void, - packageNameWithSuffix: string, packageName: string, adbPath: string, - mainActivity: string, + args: Flags, ) { + const appId = args.appId || args.appIdSuffix; + const packageNameWithSuffix = appId ? `${packageName}.${appId}` : packageName; + try { const adbArgs = [ 'shell', 'am', 'start', '-n', - `${packageNameWithSuffix}/${packageName}.${mainActivity}`, + `${packageNameWithSuffix}/${packageName}.${args.mainActivity}`, ]; if (device) { adbArgs.unshift('-s', device); @@ -31,7 +34,7 @@ function tryLaunchAppOnDevice( logger.info('Starting the app...'); } logger.debug(`Running command "${adbPath} ${adbArgs.join(' ')}"`); - spawnSync(adbPath, adbArgs, {stdio: 'inherit'}); + execa.sync(adbPath, adbArgs, {stdio: 'inherit'}); } catch (error) { throw new CLIError('Failed to start the app.', error); } diff --git a/packages/platform-android/src/config/__tests__/findAndroidAppFolder.test.ts b/packages/platform-android/src/config/__tests__/findAndroidAppFolder.test.ts deleted file mode 100644 index 1dfa3c964..000000000 --- a/packages/platform-android/src/config/__tests__/findAndroidAppFolder.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import findAndroidAppFolder from '../findAndroidAppFolder'; -import * as mocks from '../__fixtures__/android'; - -jest.mock('path'); -jest.mock('fs'); - -const fs = require('fs'); - -describe('android::findAndroidAppFolder', () => { - beforeAll(() => { - fs.__setMockFilesystem({ - empty: {}, - nested: { - android: { - app: mocks.valid, - }, - }, - flat: { - android: mocks.valid, - }, - }); - }); - - it('returns an android app folder if it exists in the given folder', () => { - expect(findAndroidAppFolder('/flat')).toBe('android'); - expect(findAndroidAppFolder('/nested')).toBe('android/app'); - }); - - it('returns `null` if there is no android app folder', () => { - expect(findAndroidAppFolder('/empty')).toBeNull(); - }); -}); diff --git a/packages/platform-android/src/config/__tests__/findAndroidDir.test.ts b/packages/platform-android/src/config/__tests__/findAndroidDir.test.ts new file mode 100644 index 000000000..2b6b27b49 --- /dev/null +++ b/packages/platform-android/src/config/__tests__/findAndroidDir.test.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import findAndroidDir from '../findAndroidDir'; +import * as mocks from '../__fixtures__/android'; + +jest.mock('path'); +jest.mock('fs'); + +const fs = require('fs'); + +describe('android::findAndroidDir', () => { + beforeAll(() => { + fs.__setMockFilesystem({ + empty: {}, + flat: { + android: mocks.valid, + }, + }); + }); + + it('returns an android folder if it exists in the given folder', () => { + expect(findAndroidDir('/flat')).toBe('android'); + }); + + it('returns null if there is no android folder', () => { + expect(findAndroidDir('/empty')).toBeNull(); + }); +}); diff --git a/packages/platform-android/src/config/__tests__/getProjectConfig.test.ts b/packages/platform-android/src/config/__tests__/getProjectConfig.test.ts index c52a63ddb..d515da617 100644 --- a/packages/platform-android/src/config/__tests__/getProjectConfig.test.ts +++ b/packages/platform-android/src/config/__tests__/getProjectConfig.test.ts @@ -55,16 +55,26 @@ describe('android::getProjectConfig', () => { const userConfig = {}; const folder = '/nested'; - expect(getProjectConfig(folder, userConfig)).not.toBeNull(); - expect(typeof getProjectConfig(folder, userConfig)).toBe('object'); + const config = getProjectConfig(folder, userConfig); + expect(config).toMatchObject({ + sourceDir: '/nested/android', + appName: 'app', + packageName: 'com.some.example', + manifestPath: '/nested/android/app/src/AndroidManifest.xml', + }); }); it('flat structure', () => { const userConfig = {}; const folder = '/flat'; - expect(getProjectConfig(folder, userConfig)).not.toBeNull(); - expect(typeof getProjectConfig(folder, userConfig)).toBe('object'); + const config = getProjectConfig(folder, userConfig); + expect(config).toMatchObject({ + sourceDir: '/flat/android', + appName: '', + packageName: 'com.some.example', + manifestPath: '/flat/android/src/AndroidManifest.xml', + }); }); it('multiple', () => { @@ -73,8 +83,13 @@ describe('android::getProjectConfig', () => { }; const folder = '/multiple'; - expect(getProjectConfig(folder, userConfig)).not.toBeNull(); - expect(typeof getProjectConfig(folder, userConfig)).toBe('object'); + const config = getProjectConfig(folder, userConfig); + expect(config).toMatchObject({ + sourceDir: '/multiple/android', + appName: '', + packageName: 'com.some.example', + manifestPath: '/multiple/android/src/main/AndroidManifest.xml', + }); }); }); diff --git a/packages/platform-android/src/config/findAndroidAppFolder.ts b/packages/platform-android/src/config/findAndroidAppFolder.ts deleted file mode 100644 index d9a3d3e92..000000000 --- a/packages/platform-android/src/config/findAndroidAppFolder.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import fs from 'fs'; -import path from 'path'; - -export default function findAndroidAppFolder(folder: string) { - const flat = 'android'; - const nested = path.join('android', 'app'); - - if (fs.existsSync(path.join(folder, nested))) { - return nested; - } - - if (fs.existsSync(path.join(folder, flat))) { - return flat; - } - - return null; -} diff --git a/packages/platform-android/src/config/findAndroidDir.ts b/packages/platform-android/src/config/findAndroidDir.ts new file mode 100644 index 000000000..4c9b32728 --- /dev/null +++ b/packages/platform-android/src/config/findAndroidDir.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import fs from 'fs'; +import path from 'path'; + +export default function findAndroidDir(root: string) { + if (fs.existsSync(path.join(root, 'android'))) { + return 'android'; + } + + return null; +} diff --git a/packages/platform-android/src/config/index.ts b/packages/platform-android/src/config/index.ts index db069bb2d..573d61ef9 100644 --- a/packages/platform-android/src/config/index.ts +++ b/packages/platform-android/src/config/index.ts @@ -7,13 +7,15 @@ */ import path from 'path'; -import findAndroidAppFolder from './findAndroidAppFolder'; +import fs from 'fs'; +import findAndroidDir from './findAndroidDir'; import findManifest from './findManifest'; import findPackageClassName from './findPackageClassName'; import readManifest from './readManifest'; import { AndroidProjectParams, AndroidDependencyParams, + AndroidProjectConfig, } from '@react-native-community/cli-types'; import {XmlDocument} from 'xmldoc'; @@ -24,20 +26,21 @@ const getPackageName = (manifest: XmlDocument) => manifest.attr.package; * defaults specified by user into consideration */ export function projectConfig( - folder: string, + root: string, userConfig: AndroidProjectParams = {}, -) { - const src = userConfig.sourceDir || findAndroidAppFolder(folder); +): AndroidProjectConfig | null { + const src = userConfig.sourceDir || findAndroidDir(root); if (!src) { return null; } - const sourceDir = path.join(folder, src); + const sourceDir = path.join(root, src); + const appName = getAppName(sourceDir, userConfig.appName); const isFlat = sourceDir.indexOf('app') === -1; const manifestPath = userConfig.manifestPath ? path.join(sourceDir, userConfig.manifestPath) - : findManifest(sourceDir); + : findManifest(path.join(sourceDir, appName)); if (!manifestPath) { return null; @@ -66,7 +69,7 @@ export function projectConfig( ); const settingsGradlePath = path.join( - folder, + root, 'android', userConfig.settingsGradlePath || 'settings.gradle', ); @@ -84,7 +87,7 @@ export function projectConfig( return { sourceDir, isFlat, - folder, + folder: root, stringsPath, manifestPath, buildGradlePath, @@ -92,27 +95,43 @@ export function projectConfig( assetsPath, mainFilePath, packageName, + packageFolder: '', + appName, }; } +function getAppName(sourceDir: string, userConfigAppName: string | undefined) { + let appName = ''; + if ( + typeof userConfigAppName === 'string' && + fs.existsSync(path.join(sourceDir, userConfigAppName)) + ) { + appName = userConfigAppName; + } else if (fs.existsSync(path.join(sourceDir, 'app'))) { + appName = 'app'; + } + return appName; +} + /** * Same as projectConfigAndroid except it returns * different config that applies to packages only */ export function dependencyConfig( - folder: string, + root: string, userConfig: AndroidDependencyParams = {}, ) { - const src = userConfig.sourceDir || findAndroidAppFolder(folder); + const src = userConfig.sourceDir || findAndroidDir(root); if (!src) { return null; } - const sourceDir = path.join(folder, src); + const sourceDir = path.join(root, src); + const appName = userConfig.appName || 'app'; const manifestPath = userConfig.manifestPath ? path.join(sourceDir, userConfig.manifestPath) - : findManifest(sourceDir); + : findManifest(path.join(sourceDir, appName)); if (!manifestPath) { return null; @@ -136,5 +155,5 @@ export function dependencyConfig( const packageInstance = userConfig.packageInstance || `new ${packageClassName}()`; - return {sourceDir, folder, packageImportPath, packageInstance}; + return {sourceDir, appName, folder: root, packageImportPath, packageInstance}; }