diff --git a/docs/autolinking.md b/docs/autolinking.md index 99cf2b666..eb8d009aa 100644 --- a/docs/autolinking.md +++ b/docs/autolinking.md @@ -36,28 +36,6 @@ The implementation ensures that a library is imported only once. If you need to See example usage in React Native template's [Podfile](https://github.com/facebook/react-native/blob/0.60-stable/template/ios/Podfile). -### Custom root (monorepos) - -The project root is where `node_modules` with `react-native` is. Autolinking script assume your project root to be `".."`, relative to `ios` directory. If you're in a project with custom structure, like this: - -``` -root/ - node_modules - example/ - ios/ -``` - -you'll need to set a custom root. Pass it as an argument to `use_native_modules!` function inside the targets and adjust the relatively required `native_modules` path accordingly: - -```rb -# example/ios/Podfile -require_relative '../../node_modules/@react-native-community/cli-platform-ios/native_modules' -target 'RNapp' do - # React pods and custom pods here... - use_native_modules!("../..") -end -``` - ## Platform Android The [native_modules.gradle](https://github.com/react-native-community/cli/blob/master/packages/platform-android/native_modules.gradle) script is included in your project's `settings.gradle` and `app/build.gradle` files and: @@ -76,31 +54,6 @@ See example usage in React Native template: - [app/build.gradle](https://github.com/facebook/react-native/blob/0.60-stable/template/android/app/build.gradle#L185) - [MainApplication.java](https://github.com/facebook/react-native/blob/769e35ba5f4c31ef913035a5cc8bc0e88546ca55/template/android/app/src/main/java/com/helloworld/MainApplication.java#L22-L28) -### Custom root (monorepos) - -The project root is where `node_modules` with `react-native` is. Autolinking scripts assume your project root to be `".."`, relative to `android` directory. If you're in a project with custom structure, like this: - -``` -root/ - node_modules - example/ - android/ -``` - -you'll need to set a custom root. Pass it as a second argument to `applyNativeModulesSettingsGradle` and `applyNativeModulesAppBuildGradle` methods and adjust the `native_modules.gradle` path accordingly: - -```groovy -// example/android/settings.gradle -apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); -applyNativeModulesSettingsGradle(settings, "../..") -``` - -```groovy -// example/android/app/build.gradle -apply from: file("../../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); -applyNativeModulesAppBuildGradle(project, "../..") -``` - ## What do I need to have in my package to make it work? You’re already using Gradle, so Android support will work by default. @@ -142,3 +95,14 @@ module.exports = { }, }; ``` + +## How can I use autolinking in a monorepo? + +There is nothing extra you need to do - monorepos are supported by default. + +Please note that in certain scenarios, such as when using Yarn workspaces, your packages might be hoisted to the root of the repository. If that is the case, please make sure that the following paths are pointing to the +correct location and update them accordingly: + +- path to `native_modules.rb` in your `ios/Podfile` +- path to `native_modules.gradle` in your `android/settings.gradle` +- path to `native_modules.gradle` in your `android/app/build.gradle` diff --git a/packages/cli-types/src/index.ts b/packages/cli-types/src/index.ts index 7738f22ec..df93441e0 100644 --- a/packages/cli-types/src/index.ts +++ b/packages/cli-types/src/index.ts @@ -39,6 +39,16 @@ export interface Command { }>; } +export type DetachedCommandFunction = ( + argv: Array, + args: Args, +) => Promise | void; + +export type DetachedCommand = Command & { + detached: true; + func: DetachedCommandFunction; +}; + interface PlatformConfig< ProjectConfig, ProjectParams, diff --git a/packages/cli/package.json b/packages/cli/package.json index 7f5e6497c..8d54bb3ab 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -42,6 +42,7 @@ "envinfo": "^7.1.0", "errorhandler": "^1.5.0", "execa": "^1.0.0", + "find-up": "^4.1.0", "fs-extra": "^7.0.1", "glob": "^7.1.1", "graceful-fs": "^4.1.3", @@ -71,8 +72,8 @@ }, "devDependencies": { "@types/command-exists": "^1.2.0", - "@types/graceful-fs": "^4.1.3", "@types/cosmiconfig": "^5.0.3", + "@types/graceful-fs": "^4.1.3", "@types/hapi__joi": "^15.0.4", "@types/minimist": "^1.2.0", "@types/mkdirp": "^0.5.2", diff --git a/packages/cli/src/cliEntry.js b/packages/cli/src/cliEntry.js index f65fc8558..5a0f2f9dd 100644 --- a/packages/cli/src/cliEntry.js +++ b/packages/cli/src/cliEntry.js @@ -14,14 +14,12 @@ import path from 'path'; import type {CommandT, ConfigT} from 'types'; // $FlowFixMe - converted to TS -import commands from './commands'; +import {detachedCommands, projectCommands} from './commands'; // $FlowFixMe - converted to TS import init from './commands/init/initCompat'; // $FlowFixMe - converted to TS import assertRequiredOptions from './tools/assertRequiredOptions'; import {logger} from '@react-native-community/cli-tools'; -// $FlowFixMe - converted to TS -import {setProjectDir} from './tools/packageManager'; import pkgJson from '../package.json'; // $FlowFixMe - converted to TS import loadConfig from './tools/config'; @@ -117,7 +115,11 @@ const addCommand = (command: CommandT, ctx: ConfigT) => { try { assertRequiredOptions(options, passedOptions); - await command.func(argv, ctx, passedOptions); + if (command.detached) { + await command.func(argv, passedOptions); + } else { + await command.func(argv, ctx, passedOptions); + } } catch (error) { handleError(error); } @@ -149,6 +151,9 @@ async function run() { } async function setupAndRun() { + // Commander is not available yet + logger.setVerbose(process.argv.includes('--verbose')); + // We only have a setup script for UNIX envs currently if (process.platform !== 'win32') { const scriptName = 'setup_env.sh'; @@ -168,19 +173,29 @@ async function setupAndRun() { } } - // when we run `config`, we don't want to output anything to the console. We - // expect it to return valid JSON - if (process.argv.includes('config')) { - logger.disable(); - } + detachedCommands.forEach(addCommand); - const ctx = loadConfig(); + try { + // when we run `config`, we don't want to output anything to the console. We + // expect it to return valid JSON + if (process.argv.includes('config')) { + logger.disable(); + } - logger.enable(); + const ctx = loadConfig(); - setProjectDir(ctx.root); + logger.enable(); - [...commands, ...ctx.commands].forEach(command => addCommand(command, ctx)); + [...projectCommands, ...ctx.commands].forEach(command => + addCommand(command, ctx), + ); + } catch (e) { + logger.enable(); + logger.debug(e.message); + logger.debug( + 'Failed to load configuration of your project. Only a subset of commands will be available.', + ); + } commander.parse(process.argv); @@ -194,8 +209,6 @@ async function setupAndRun() { if (commander.args.length === 0 && commander.rawArgs.includes('--version')) { console.log(pkgJson.version); } - - logger.setVerbose(commander.verbose); } export default { diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 64dce9ebc..bf0423856 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -1,4 +1,4 @@ -import {Command} from '@react-native-community/cli-types'; +import {DetachedCommand, Command} from '@react-native-community/cli-types'; // @ts-ignore - JS file import server from './server/server'; @@ -15,7 +15,7 @@ import init from './init'; // @ts-ignore - JS file import doctor from './doctor'; -export default [ +export const projectCommands = [ server, bundle, ramBundle, @@ -26,6 +26,7 @@ export default [ upgrade, info, config, - init, doctor, ] as Command[]; + +export const detachedCommands = [init] as DetachedCommand[]; diff --git a/packages/cli/src/commands/init/__tests__/template.test.ts b/packages/cli/src/commands/init/__tests__/template.test.ts index 472e2e864..a727d44aa 100644 --- a/packages/cli/src/commands/init/__tests__/template.test.ts +++ b/packages/cli/src/commands/init/__tests__/template.test.ts @@ -26,7 +26,7 @@ test('installTemplatePackage', async () => { expect(PackageManger.install).toHaveBeenCalledWith([TEMPLATE_NAME], { preferYarn: false, silent: true, - cwd: TEMPLATE_SOURCE_DIR, + root: TEMPLATE_SOURCE_DIR, }); }); diff --git a/packages/cli/src/commands/init/index.ts b/packages/cli/src/commands/init/index.ts index 4b76ba7f3..244446385 100644 --- a/packages/cli/src/commands/init/index.ts +++ b/packages/cli/src/commands/init/index.ts @@ -2,6 +2,7 @@ import init from './init'; export default { func: init, + detached: true, name: 'init ', description: 'Initialize a new React Native project named in a directory of the same name.', diff --git a/packages/cli/src/commands/init/init.ts b/packages/cli/src/commands/init/init.ts index 14459a898..3b524fa24 100644 --- a/packages/cli/src/commands/init/init.ts +++ b/packages/cli/src/commands/init/init.ts @@ -11,7 +11,6 @@ import {validateProjectName} from './validate'; import DirectoryAlreadyExistsError from './errors/DirectoryAlreadyExistsError'; import printRunInstructions from './printRunInstructions'; import {CLIError, logger} from '@react-native-community/cli-tools'; -import {Config} from '@react-native-community/cli-types'; import { installTemplatePackage, getTemplateConfig, @@ -47,16 +46,6 @@ function doesDirectoryExist(dir: string) { return fs.existsSync(dir); } -function getProjectDirectory({ - projectName, - directory, -}: { - projectName: string; - directory: string; -}): string { - return path.relative(process.cwd(), directory || projectName); -} - async function setProjectDirectory(directory: string) { const directoryExists = doesDirectoryExist(directory); if (directoryExists) { @@ -87,6 +76,8 @@ async function setProjectDirectory(directory: string) { error, ); } + + return process.cwd(); } function adjustNameIfUrl(name: string, cwd: string) { @@ -112,7 +103,7 @@ async function createFromTemplate({ logger.debug('Initializing new project'); logger.log(banner); - await setProjectDirectory(directory); + const projectDirectory = await setProjectDirectory(directory); const Loader = getLoader(); const loader = new Loader({text: 'Downloading template'}); @@ -152,7 +143,12 @@ async function createFromTemplate({ loader.succeed(); } - await installDependencies({projectName, npm, loader}); + await installDependencies({ + projectName, + npm, + loader, + root: projectDirectory, + }); } catch (e) { loader.fail(); throw new Error(e); @@ -165,16 +161,19 @@ async function installDependencies({ projectName, npm, loader, + root, }: { projectName: string; npm?: boolean; loader: ora.Ora; + root: string; }) { loader.start('Installing dependencies'); await PackageManager.installAll({ preferYarn: !npm, silent: true, + root, }); if (process.platform === 'darwin') { @@ -213,10 +212,9 @@ async function createProject( export default (async function initialize( [projectName]: Array, - context: Config, options: Options, ) { - const rootFolder = context.root; + const root = process.cwd(); validateProjectName(projectName); @@ -226,15 +224,12 @@ export default (async function initialize( */ const version: string = minimist(process.argv).version || DEFAULT_VERSION; - const directoryName = getProjectDirectory({ - projectName, - directory: options.directory || projectName, - }); + const directoryName = path.relative(root, options.directory || projectName); try { await createProject(projectName, directoryName, version, options); - const projectFolder = path.join(rootFolder, projectName); + const projectFolder = path.join(root, projectName); printRunInstructions(projectFolder, projectName); } catch (e) { logger.error(e.message); diff --git a/packages/cli/src/commands/init/initCompat.ts b/packages/cli/src/commands/init/initCompat.ts index 15c475e9f..fc9592f2c 100644 --- a/packages/cli/src/commands/init/initCompat.ts +++ b/packages/cli/src/commands/init/initCompat.ts @@ -56,7 +56,6 @@ async function generateProject( const pkgJson = require('react-native/package.json'); const reactVersion = pkgJson.peerDependencies.react; - PackageManager.setProjectDir(destinationRoot); await createProjectFromTemplate( destinationRoot, newProjectName, @@ -64,19 +63,26 @@ async function generateProject( ); logger.info('Adding required dependencies'); - await PackageManager.install([`react@${reactVersion}`]); + await PackageManager.install([`react@${reactVersion}`], { + root: destinationRoot, + }); logger.info('Adding required dev dependencies'); - await PackageManager.installDev([ - '@babel/core', - '@babel/runtime', - '@react-native-community/eslint-config', - 'eslint', - 'jest', - 'babel-jest', - 'metro-react-native-babel-preset', - `react-test-renderer@${reactVersion}`, - ]); + await PackageManager.installDev( + [ + '@babel/core', + '@babel/runtime', + '@react-native-community/eslint-config', + 'eslint', + 'jest', + 'babel-jest', + 'metro-react-native-babel-preset', + `react-test-renderer@${reactVersion}`, + ], + { + root: destinationRoot, + }, + ); addJestToPackageJson(destinationRoot); diff --git a/packages/cli/src/commands/init/template.ts b/packages/cli/src/commands/init/template.ts index 683d24c71..02e6831ba 100644 --- a/packages/cli/src/commands/init/template.ts +++ b/packages/cli/src/commands/init/template.ts @@ -14,14 +14,14 @@ export type TemplateConfig = { export function installTemplatePackage( templateName: string, - cwd: string, + root: string, npm?: boolean, ) { logger.debug(`Installing template from ${templateName}`); return PackageManager.install([templateName], { preferYarn: !npm, silent: true, - cwd, + root, }); } diff --git a/packages/cli/src/commands/install/install.ts b/packages/cli/src/commands/install/install.ts index 6f3a8fbce..a630247f7 100644 --- a/packages/cli/src/commands/install/install.ts +++ b/packages/cli/src/commands/install/install.ts @@ -9,12 +9,13 @@ import {logger} from '@react-native-community/cli-tools'; import * as PackageManager from '../../tools/packageManager'; import link from '../link/link'; import loadConfig from '../../tools/config'; +import {Config} from '@react-native-community/cli-types'; -async function install(args: Array): Promise { +async function install(args: Array, ctx: Config): Promise { const name = args[0]; logger.info(`Installing "${name}"...`); - await PackageManager.install([name]); + await PackageManager.install([name], {root: ctx.root}); // Reload configuration to see newly installed dependency const newConfig = loadConfig(); diff --git a/packages/cli/src/commands/install/uninstall.ts b/packages/cli/src/commands/install/uninstall.ts index a82e6c71a..f5c46bf1b 100644 --- a/packages/cli/src/commands/install/uninstall.ts +++ b/packages/cli/src/commands/install/uninstall.ts @@ -17,7 +17,7 @@ async function uninstall(args: Array, ctx: Config): Promise { await unlink.func([name], ctx, {}); logger.info(`Uninstalling "${name}"...`); - await PackageManager.uninstall([name]); + await PackageManager.uninstall([name], {root: ctx.root}); logger.success(`Successfully uninstalled and unlinked "${name}"`); } diff --git a/packages/cli/src/commands/upgrade/upgrade.ts b/packages/cli/src/commands/upgrade/upgrade.ts index 1b419e0cb..162644bba 100644 --- a/packages/cli/src/commands/upgrade/upgrade.ts +++ b/packages/cli/src/commands/upgrade/upgrade.ts @@ -138,7 +138,7 @@ const getVersionToUpgradeTo = async ( return newVersion; }; -const installDeps = async (newVersion: string) => { +const installDeps = async (root: string, newVersion: string) => { logger.info( `Installing "react-native@${newVersion}" and its peer dependencies...`, ); @@ -149,6 +149,7 @@ const installDeps = async (newVersion: string) => { ]; await PackageManager.install(deps, { silent: true, + root, }); await execa('git', ['add', 'package.json']); try { @@ -314,7 +315,7 @@ async function upgrade(argv: Array, ctx: Config) { if (patch === '') { logger.info('Diff has no changes to apply, proceeding further'); - await installDeps(newVersion); + await installDeps(projectDir, newVersion); await installCocoaPodsDeps(projectDir, thirdPartyIOSDeps); logger.success( @@ -341,7 +342,7 @@ async function upgrade(argv: Array, ctx: Config) { logger.warn( 'Continuing after failure. Some of the files are upgraded but you will need to deal with conflicts manually', ); - await installDeps(newVersion); + await installDeps(projectDir, newVersion); logger.info('Running "git status" to check what changed...'); await execa('git', ['status'], {stdio: 'inherit'}); } else { @@ -350,7 +351,7 @@ async function upgrade(argv: Array, ctx: Config) { ); } } else { - await installDeps(newVersion); + await installDeps(projectDir, newVersion); await installCocoaPodsDeps(projectDir, thirdPartyIOSDeps); logger.info('Running "git status" to check what changed...'); await execa('git', ['status'], {stdio: 'inherit'}); diff --git a/packages/cli/src/tools/__tests__/packageManager-test.ts b/packages/cli/src/tools/__tests__/packageManager-test.ts index 8bde1bbd2..1339f9c2f 100644 --- a/packages/cli/src/tools/__tests__/packageManager-test.ts +++ b/packages/cli/src/tools/__tests__/packageManager-test.ts @@ -5,8 +5,8 @@ import {logger} from '@react-native-community/cli-tools'; import * as PackageManager from '../packageManager'; const PACKAGES = ['react', 'react-native']; -const EXEC_OPTS = {stdio: 'inherit'}; const PROJECT_ROOT = '/some/dir'; +const EXEC_OPTS = {stdio: 'inherit', cwd: PROJECT_ROOT}; afterEach(() => { jest.resetAllMocks(); @@ -22,13 +22,13 @@ describe('yarn', () => { }); it('should install', () => { - PackageManager.install(PACKAGES, {preferYarn: true}); + PackageManager.install(PACKAGES, {preferYarn: true, root: PROJECT_ROOT}); expect(execa).toHaveBeenCalledWith('yarn', ['add', ...PACKAGES], EXEC_OPTS); }); it('should installDev', () => { - PackageManager.installDev(PACKAGES, {preferYarn: true}); + PackageManager.installDev(PACKAGES, {preferYarn: true, root: PROJECT_ROOT}); expect(execa).toHaveBeenCalledWith( 'yarn', @@ -38,7 +38,7 @@ describe('yarn', () => { }); it('should uninstall', () => { - PackageManager.uninstall(PACKAGES, {preferYarn: true}); + PackageManager.uninstall(PACKAGES, {preferYarn: true, root: PROJECT_ROOT}); expect(execa).toHaveBeenCalledWith( 'yarn', @@ -50,7 +50,7 @@ describe('yarn', () => { describe('npm', () => { it('should install', () => { - PackageManager.install(PACKAGES, {preferYarn: false}); + PackageManager.install(PACKAGES, {preferYarn: false, root: PROJECT_ROOT}); expect(execa).toHaveBeenCalledWith( 'npm', @@ -60,7 +60,10 @@ describe('npm', () => { }); it('should installDev', () => { - PackageManager.installDev(PACKAGES, {preferYarn: false}); + PackageManager.installDev(PACKAGES, { + preferYarn: false, + root: PROJECT_ROOT, + }); expect(execa).toHaveBeenCalledWith( 'npm', @@ -70,7 +73,7 @@ describe('npm', () => { }); it('should uninstall', () => { - PackageManager.uninstall(PACKAGES, {preferYarn: false}); + PackageManager.uninstall(PACKAGES, {preferYarn: false, root: PROJECT_ROOT}); expect(execa).toHaveBeenCalledWith( 'npm', @@ -82,7 +85,7 @@ describe('npm', () => { it('should use npm if yarn is not available', () => { jest.spyOn(yarn, 'getYarnVersionIfAvailable').mockImplementation(() => false); - PackageManager.install(PACKAGES, {preferYarn: true}); + PackageManager.install(PACKAGES, {preferYarn: true, root: PROJECT_ROOT}); expect(execa).toHaveBeenCalledWith( 'npm', @@ -94,8 +97,7 @@ it('should use npm if yarn is not available', () => { it('should use npm if project is not using yarn', () => { jest.spyOn(yarn, 'isProjectUsingYarn').mockImplementation(() => false); - PackageManager.setProjectDir(PROJECT_ROOT); - PackageManager.install(PACKAGES); + PackageManager.install(PACKAGES, {root: PROJECT_ROOT}); expect(execa).toHaveBeenCalledWith( 'npm', @@ -109,8 +111,7 @@ it('should use yarn if project is using yarn', () => { jest.spyOn(yarn, 'getYarnVersionIfAvailable').mockImplementation(() => true); jest.spyOn(yarn, 'isProjectUsingYarn').mockImplementation(() => true); - PackageManager.setProjectDir(PROJECT_ROOT); - PackageManager.install(PACKAGES); + PackageManager.install(PACKAGES, {root: PROJECT_ROOT}); expect(execa).toHaveBeenCalledWith('yarn', ['add', ...PACKAGES], EXEC_OPTS); expect(yarn.isProjectUsingYarn).toHaveBeenCalledWith(PROJECT_ROOT); @@ -125,10 +126,11 @@ test.each([[false, 'pipe'], [true, 'inherit']])( jest.spyOn(yarn, 'isProjectUsingYarn').mockImplementation(() => true); jest.spyOn(logger, 'isVerbose').mockImplementation(() => isVerbose); - PackageManager.install(PACKAGES, {silent: true}); + PackageManager.install(PACKAGES, {root: PROJECT_ROOT, silent: true}); expect(execa).toHaveBeenCalledWith('yarn', ['add', ...PACKAGES], { stdio: stdioType, + cwd: PROJECT_ROOT, }); }, ); diff --git a/packages/cli/src/tools/config/__tests__/findProjectRoot-test.ts b/packages/cli/src/tools/config/__tests__/findProjectRoot-test.ts new file mode 100644 index 000000000..4504a2bfb --- /dev/null +++ b/packages/cli/src/tools/config/__tests__/findProjectRoot-test.ts @@ -0,0 +1,36 @@ +import path from 'path'; +import findProjectRoot from '../findProjectRoot'; +import { + cleanup, + writeFiles, + getTempDirectory, +} from '../../../../../../jest/helpers'; + +beforeEach(() => { + cleanup(DIR); + jest.resetModules(); +}); + +afterEach(() => cleanup(DIR)); + +const DIR = getTempDirectory('find_project_root_test'); + +test('resolves to correct project root', () => { + writeFiles(DIR, { + 'package.json': '{}', + 'ios/Podfile': '', + }); + const cwd = path.join(DIR, 'ios'); + expect(findProjectRoot(cwd)).toBe(DIR); + expect(findProjectRoot(DIR)).toBe(DIR); +}); + +test('resolves to correct project root in a monorepo', () => { + writeFiles(DIR, { + 'package.json': '{}', + 'packages/mobile/package.json': '{}', + 'packages/mobile/ios/Podfile': '', + }); + const cwd = path.join(DIR, 'packages/mobile/ios'); + expect(findProjectRoot(cwd)).toBe(path.join(DIR, 'packages/mobile')); +}); diff --git a/packages/cli/src/tools/config/findProjectRoot.ts b/packages/cli/src/tools/config/findProjectRoot.ts new file mode 100644 index 000000000..32b3c2e28 --- /dev/null +++ b/packages/cli/src/tools/config/findProjectRoot.ts @@ -0,0 +1,26 @@ +import findUp from 'find-up'; +import path from 'path'; +import {CLIError} from '@react-native-community/cli-tools'; + +/** + * Finds project root by looking for a closest `package.json`. + */ +export default function findProjectRoot(cwd = process.cwd()): string { + const packageLocation = findUp.sync('package.json', {cwd}); + + /** + * It is possible that `package.json` doesn't exist + * in the tree. In that case, we want to throw an error. + * + * When executing via `npx`, this will never happen as `npm` + * requires that file to be present in order to run. + */ + if (!packageLocation) { + throw new CLIError(` + We couldn't find a package.json in your project. + Are you sure you are running it inside a React Native project? + `); + } + + return path.dirname(packageLocation); +} diff --git a/packages/cli/src/tools/config/index.ts b/packages/cli/src/tools/config/index.ts index f5b1f1ca2..f0a5d1b2b 100644 --- a/packages/cli/src/tools/config/index.ts +++ b/packages/cli/src/tools/config/index.ts @@ -11,6 +11,7 @@ import {logger, inlineString} from '@react-native-community/cli-tools'; import * as ios from '@react-native-community/cli-platform-ios'; import * as android from '@react-native-community/cli-platform-android'; import findDependencies from './findDependencies'; +import findProjectRoot from './findProjectRoot'; import resolveReactNativePath from './resolveReactNativePath'; import findAssets from './findAssets'; import { @@ -61,7 +62,7 @@ function getDependencyConfig( /** * Loads CLI configuration */ -function loadConfig(projectRoot: string = process.cwd()): Config { +function loadConfig(projectRoot: string = findProjectRoot()): Config { let lazyProject: ProjectConfig; const userConfig = readConfigFromDisk(projectRoot); diff --git a/packages/cli/src/tools/generator/templates.ts b/packages/cli/src/tools/generator/templates.ts index 4ff6370af..c538944fb 100644 --- a/packages/cli/src/tools/generator/templates.ts +++ b/packages/cli/src/tools/generator/templates.ts @@ -71,7 +71,7 @@ async function createFromRemoteTemplate( // Check if the template exists logger.info(`Fetching template ${installPackage}...`); try { - await PackageManager.install([installPackage]); + await PackageManager.install([installPackage], {root: destPath}); const templatePath = path.resolve('node_modules', templateName); copyProjectTemplateAndReplace(templatePath, destPath, newProjectName, { // Every template contains a dummy package.json file included @@ -84,12 +84,12 @@ async function createFromRemoteTemplate( 'devDependencies.json', ], }); - await installTemplateDependencies(templatePath); - await installTemplateDevDependencies(templatePath); + await installTemplateDependencies(templatePath, destPath); + await installTemplateDevDependencies(templatePath, destPath); } finally { // Clean up the temp files try { - await PackageManager.uninstall([templateName]); + await PackageManager.uninstall([templateName], {root: destPath}); } catch (err) { // Not critical but we still want people to know and report // if this the clean up fails. @@ -101,7 +101,7 @@ async function createFromRemoteTemplate( } } -async function installTemplateDependencies(templatePath: string) { +async function installTemplateDependencies(templatePath: string, root: string) { // dependencies.json is a special file that lists additional dependencies // that are required by this template const dependenciesJsonPath = path.resolve(templatePath, 'dependencies.json'); @@ -122,12 +122,15 @@ async function installTemplateDependencies(templatePath: string) { const dependenciesToInstall = Object.keys(dependencies).map( depName => `${depName}@${dependencies[depName]}`, ); - await PackageManager.install(dependenciesToInstall); + await PackageManager.install(dependenciesToInstall, {root}); logger.info("Linking native dependencies into the project's build files..."); - execSync('react-native link', {stdio: 'inherit'}); + execSync('react-native link', {cwd: root, stdio: 'inherit'}); } -async function installTemplateDevDependencies(templatePath: string) { +async function installTemplateDevDependencies( + templatePath: string, + root: string, +) { // devDependencies.json is a special file that lists additional develop dependencies // that are required by this template const devDependenciesJsonPath = path.resolve( @@ -152,7 +155,7 @@ async function installTemplateDevDependencies(templatePath: string) { const dependenciesToInstall = Object.keys(dependencies).map( depName => `${depName}@${dependencies[depName]}`, ); - await PackageManager.installDev(dependenciesToInstall); + await PackageManager.installDev(dependenciesToInstall, {root}); } export {createProjectFromTemplate}; diff --git a/packages/cli/src/tools/packageManager.ts b/packages/cli/src/tools/packageManager.ts index 15282cef3..bfdf40094 100644 --- a/packages/cli/src/tools/packageManager.ts +++ b/packages/cli/src/tools/packageManager.ts @@ -5,11 +5,9 @@ import {getYarnVersionIfAvailable, isProjectUsingYarn} from './yarn'; type Options = { preferYarn?: boolean; silent?: boolean; - cwd?: string; + root: string; }; -let projectDir: string; - const packageManagers = { yarn: { install: ['add'], @@ -28,7 +26,7 @@ const packageManagers = { function configurePackageManager( packageNames: Array, action: 'install' | 'installDev' | 'installAll' | 'uninstall', - options?: Options, + options: Options, ) { const pm = shouldUseYarn(options) ? 'yarn' : 'npm'; const [executable, ...flags] = packageManagers[pm][action]; @@ -39,39 +37,34 @@ function configurePackageManager( function executeCommand( command: string, args: Array, - options?: Options, + options: Options, ) { return execa(command, args, { - stdio: - options && options.silent && !logger.isVerbose() ? 'pipe' : 'inherit', - cwd: options && options.cwd, + stdio: options.silent && !logger.isVerbose() ? 'pipe' : 'inherit', + cwd: options.root, }); } -function shouldUseYarn(options?: Options) { +function shouldUseYarn(options: Options) { if (options && options.preferYarn !== undefined) { return options.preferYarn && getYarnVersionIfAvailable(); } - return isProjectUsingYarn(projectDir) && getYarnVersionIfAvailable(); -} - -export function setProjectDir(dir: string) { - projectDir = dir; + return isProjectUsingYarn(options.root) && getYarnVersionIfAvailable(); } -export function install(packageNames: Array, options?: Options) { +export function install(packageNames: Array, options: Options) { return configurePackageManager(packageNames, 'install', options); } -export function installDev(packageNames: Array, options?: Options) { +export function installDev(packageNames: Array, options: Options) { return configurePackageManager(packageNames, 'installDev', options); } -export function uninstall(packageNames: Array, options?: Options) { +export function uninstall(packageNames: Array, options: Options) { return configurePackageManager(packageNames, 'uninstall', options); } -export function installAll(options?: Options) { +export function installAll(options: Options) { return configurePackageManager([], 'installAll', options); } diff --git a/packages/cli/src/tools/yarn.ts b/packages/cli/src/tools/yarn.ts index cb971e61c..45a8f463a 100644 --- a/packages/cli/src/tools/yarn.ts +++ b/packages/cli/src/tools/yarn.ts @@ -7,10 +7,9 @@ */ import {execSync} from 'child_process'; -import fs from 'fs'; -import path from 'path'; import semver from 'semver'; import {logger} from '@react-native-community/cli-tools'; +import findUp from 'find-up'; /** * Use Yarn if available, it's much faster than the npm client. @@ -41,12 +40,8 @@ export function getYarnVersionIfAvailable() { } /** - * Check that 'react-native init' itself used yarn to install React Native. - * When using an old global react-native-cli@1.0.0 (or older), we don't want - * to install React Native with npm, and React + Jest with yarn. - * Let's be safe and not mix yarn and npm in a single project. - * @param projectDir e.g. /Users/martin/AwesomeApp + * Check if project is using Yarn (has `yarn.lock` in the tree) */ -export function isProjectUsingYarn(projectDir: string) { - return fs.existsSync(path.join(projectDir, 'yarn.lock')); +export function isProjectUsingYarn(cwd: string) { + return findUp.sync('yarn.lock', {cwd}); } diff --git a/packages/platform-android/native_modules.gradle b/packages/platform-android/native_modules.gradle index f9bf5dd7a..2369c2bdc 100644 --- a/packages/platform-android/native_modules.gradle +++ b/packages/platform-android/native_modules.gradle @@ -69,38 +69,23 @@ public class PackageList { class ReactNativeModules { private Logger logger - private Project project private String packageName - private DefaultSettings defaultSettings - private String root private ArrayList> reactNativeModules private static String LOG_PREFIX = ":ReactNative:" ReactNativeModules(Logger logger) { this.logger = logger - } - - void applySettingsGradle(DefaultSettings defaultSettings, String root) { - this.defaultSettings = defaultSettings - this.root = root - this.reactNativeModules = this.getReactNativeConfig() - - addReactNativeModuleProjects() - } - - void applyBuildGradle(Project project, String root) { - this.project = project - this.root = root - this.reactNativeModules = this.getReactNativeConfig() - - addReactNativeModuleDependencies() + + def (nativeModules, packageName) = this.getReactNativeConfig() + this.reactNativeModules = nativeModules + this.packageName = packageName } /** * Include the react native modules android projects and specify their project directory */ - void addReactNativeModuleProjects() { + void addReactNativeModuleProjects(DefaultSettings defaultSettings) { reactNativeModules.forEach { reactNativeModule -> String nameCleansed = reactNativeModule["nameCleansed"] String androidSourceDir = reactNativeModule["androidSourceDir"] @@ -112,33 +97,16 @@ class ReactNativeModules { /** * Adds the react native modules as dependencies to the users `app` project */ - void addReactNativeModuleDependencies() { + void addReactNativeModuleDependencies(Project appProject) { reactNativeModules.forEach { reactNativeModule -> def nameCleansed = reactNativeModule["nameCleansed"] - project.dependencies { + appProject.dependencies { // TODO(salakar): are other dependency scope methods such as `api` required? implementation project(path: ":${nameCleansed}") } } } - /** - * Returns the user's project root (i.e. where the node_modules dir is located). - */ - File getReactNativeProjectRoot() { - File androidRoot - - if (this.project) { - androidRoot = this.project.rootProject.projectDir - } else { - androidRoot = this.defaultSettings.rootProject.projectDir - } - - File rnRoot = new File(androidRoot, this.root) - this.logger.debug("${LOG_PREFIX}Using React Native project root path '${rnRoot.toString()}'") - return rnRoot - } - /** * Code-gen a java file with all the detected ReactNativePackage instances automatically added * @@ -182,12 +150,11 @@ class ReactNativeModules { ArrayList> reactNativeModules = new ArrayList>() def cmdProcess - def root = getReactNativeProjectRoot() - def command = "node ./node_modules/react-native/cli.js config" + def command = "npx --quiet react-native config" def reactNativeConfigOutput = "" try { - cmdProcess = Runtime.getRuntime().exec(command, null, root) + cmdProcess = Runtime.getRuntime().exec(command) def bufferedReader = new BufferedReader(new InputStreamReader(cmdProcess.getInputStream())) def buff = "" def readBuffer = new StringBuffer() @@ -211,7 +178,6 @@ class ReactNativeModules { } def json = new JsonSlurper().parseText(reactNativeConfigOutput) - this.packageName = json["project"]["android"]["packageName"] def dependencies = json["dependencies"] dependencies.each { name, value -> @@ -235,7 +201,7 @@ class ReactNativeModules { } } - return reactNativeModules + return [reactNativeModules, json["project"]["android"]["packageName"]]; } } @@ -245,12 +211,20 @@ class ReactNativeModules { def autoModules = new ReactNativeModules(logger) -ext.applyNativeModulesSettingsGradle = { DefaultSettings defaultSettings, String root = ".." -> - autoModules.applySettingsGradle(defaultSettings, root) +ext.applyNativeModulesSettingsGradle = { DefaultSettings defaultSettings, String root = null -> + if (root != null) { + logger.warn("${ReactNativeModules.LOG_PREFIX}Passing custom root is deprecated. CLI detects root automatically now."); + logger.warn("${ReactNativeModules.LOG_PREFIX}Please remove second argument to `applyNativeModulesSettingsGradle`."); + } + autoModules.addReactNativeModuleProjects(defaultSettings) } -ext.applyNativeModulesAppBuildGradle = { Project project, String root = ".." -> - autoModules.applyBuildGradle(project, root) +ext.applyNativeModulesAppBuildGradle = { Project project, String root = null -> + if (root != null) { + logger.warn("${ReactNativeModules.LOG_PREFIX}Passing custom root is deprecated. CLI detects root automatically now"); + logger.warn("${ReactNativeModules.LOG_PREFIX}Please remove second argument to `applyNativeModulesAppBuildGradle`."); + } + autoModules.addReactNativeModuleDependencies(project) def generatedSrcDir = new File(buildDir, "generated/rncli/src/main/java") def generatedCodeDir = new File(generatedSrcDir, generatedFilePackage.replace('.', '/')) diff --git a/packages/platform-ios/native_modules.rb b/packages/platform-ios/native_modules.rb index d42cc26b1..431282afb 100644 --- a/packages/platform-ios/native_modules.rb +++ b/packages/platform-ios/native_modules.rb @@ -5,24 +5,30 @@ # require 'pathname' -def use_native_modules!(root = "..", config = nil) +def use_native_modules!(config = nil) + if (config.is_a? String) + Pod::UI.warn("Passing custom root to use_native_modules! is deprecated.", + [ + "CLI detects root of the project automatically. The \"#{config}\" argument was ignored.", + ]); + config = nil; + end + if (!config) json = [] - # Make sure `react-native config` is ran from your project root - Dir.chdir(root) do - IO.popen("./node_modules/.bin/react-native config") do |data| - while line = data.gets - json << line - end + IO.popen("npx --quiet react-native config") do |data| + while line = data.gets + json << line end end config = JSON.parse(json.join("\n")) end + project_root = Pathname.new(config["project"]["ios"]["sourceDir"]) + packages = config["dependencies"] - config_root = config["root"] found_pods = [] packages.each do |package_name, package| @@ -57,10 +63,10 @@ def use_native_modules!(root = "..", config = nil) end podspec_dir_path = Pathname.new(File.dirname(podspec_path)) - project_root = Pathname.new(config_root) + relative_path = podspec_dir_path.relative_path_from project_root - pod spec.name, :path => File.join(root, relative_path) + pod spec.name, :path => relative_path.to_path if package_config["scriptPhases"] # Can be either an object, or an array of objects @@ -129,7 +135,6 @@ def pluralize(count) "execution_position" => "before_compile", "input" => "string" } - @ios_package = ios_package = { "root" => "/root/app/node_modules/react", "platforms" => { @@ -148,8 +153,13 @@ def pluralize(count) }, } } + @project = { + "ios" => { + "sourceDir" => "/root/app/ios" + } + } @config = { - "root" => "/root/app", + "project" => @project, "dependencies" => { "ios-dep" => @ios_package, "android-dep" => @android_package @@ -166,8 +176,8 @@ def pluralize(count) spec.singleton_class.send(:define_method, :name) { "ios-dep" } - podfile.singleton_class.send(:define_method, :use_native_modules) do |path, config| - use_native_modules!('..', config) + podfile.singleton_class.send(:define_method, :use_native_modules) do |config| + use_native_modules!(config) end Pod::Specification.singleton_class.send(:define_method, :from_file) do |podspec_path| @@ -197,7 +207,7 @@ def pluralize(count) end it "activates iOS pods" do - @podfile.use_native_modules('..', @config) + @podfile.use_native_modules(@config) @activated_pods.must_equal [{ name: "ios-dep", options: { path: "../node_modules/react" } @@ -208,7 +218,7 @@ def pluralize(count) activated_pod = Object.new activated_pod.singleton_class.send(:define_method, :name) { "ios-dep" } @current_target_definition_dependencies << activated_pod - @podfile.use_native_modules('..', @config) + @podfile.use_native_modules(@config) @activated_pods.must_equal [] end @@ -216,15 +226,15 @@ def pluralize(count) activated_pod = Object.new activated_pod.singleton_class.send(:define_method, :name) { "ios-dep/foo/bar" } @current_target_definition_dependencies << activated_pod - @podfile.use_native_modules('..', @config) + @podfile.use_native_modules(@config) @activated_pods.must_equal [] end it "prints out the native module pods that were found" do - @podfile.use_native_modules('..', { "root" => "/root/app", "dependencies" => {} }) - @podfile.use_native_modules('..', { "root" => "/root/app", "dependencies" => { "pkg-1" => @ios_package }}) - @podfile.use_native_modules('..', { - "root" => "/root/app", "dependencies" => { "pkg-1" => @ios_package, "pkg-2" => @ios_package } + @podfile.use_native_modules({ "project" => @project, "dependencies" => {} }) + @podfile.use_native_modules({ "project" => @project, "dependencies" => { "pkg-1" => @ios_package }}) + @podfile.use_native_modules({ + "project" => @project, "dependencies" => { "pkg-1" => @ios_package, "pkg-2" => @ios_package } }) @printed_messages.must_equal [ "Detected React Native module pod for ios-dep", @@ -235,7 +245,7 @@ def pluralize(count) describe "concerning script_phases" do it "uses the options directly" do @config["dependencies"]["ios-dep"]["platforms"]["ios"]["scriptPhases"] = [@script_phase] - @podfile.use_native_modules('..', @config) + @podfile.use_native_modules(@config) @added_scripts.must_equal [{ :script => "123", :name => "My Name", @@ -253,7 +263,7 @@ def pluralize(count) file_read_mock.expect(:call, "contents from file", [File.join(@ios_package["root"], "some_shell_script.sh")]) File.stub(:read, file_read_mock) do - @podfile.use_native_modules('..', @config) + @podfile.use_native_modules(@config) end @added_scripts.must_equal [{ diff --git a/types/index.js b/types/index.js index 1c09e0a3e..025d2ef11 100644 --- a/types/index.js +++ b/types/index.js @@ -4,6 +4,7 @@ export type CommandT = { name: string, description?: string, + detached?: boolean, func: (argv: Array, ctx: ConfigT, args: Object) => ?Promise, options?: Array<{ name: string, diff --git a/yarn.lock b/yarn.lock index 08af35747..dd6834730 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4367,6 +4367,14 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" +find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + flat-cache@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.0.tgz#d3030b32b38154f4e3b7e9c709f490f7ef97c481" @@ -6123,6 +6131,13 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + lodash._reinterpolate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" @@ -7317,6 +7332,13 @@ p-limit@^2.0.0: dependencies: p-try "^2.0.0" +p-limit@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.1.tgz#aa07a788cc3151c939b5131f63570f0dd2009537" + integrity sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg== + dependencies: + p-try "^2.0.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -7330,6 +7352,13 @@ p-locate@^3.0.0: dependencies: p-limit "^2.0.0" +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + p-map-series@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-map-series/-/p-map-series-1.0.0.tgz#bf98fe575705658a9e1351befb85ae4c1f07bdca" @@ -7491,6 +7520,11 @@ path-exists@^3.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"