diff --git a/__e2e__/config.test.ts b/__e2e__/config.test.ts index 477a38984..f5e8e4baa 100644 --- a/__e2e__/config.test.ts +++ b/__e2e__/config.test.ts @@ -49,7 +49,7 @@ beforeAll(() => { // Initialise React Native project - runCLI(DIR, ['init', 'TestProject']); + runCLI(DIR, ['init', 'TestProject', '--install-pods']); // Link CLI to the project const pkgs = [ diff --git a/__e2e__/init.test.ts b/__e2e__/init.test.ts index 845ac7908..384784d7b 100644 --- a/__e2e__/init.test.ts +++ b/__e2e__/init.test.ts @@ -61,7 +61,14 @@ test('init fails if the directory already exists', () => { test('init should prompt for the project name', () => { createCustomTemplateFiles(); - const {stdout} = runCLI(DIR, ['init', 'test', '--template', templatePath]); + const {stdout} = runCLI(DIR, [ + 'init', + 'test', + '--template', + templatePath, + '--install-pods', + 'false', + ]); (prompts as jest.MockedFunction).mockReturnValue( Promise.resolve({ @@ -79,6 +86,8 @@ test('init --template filepath', () => { '--template', templatePath, 'TestInit', + '--install-pods', + 'false', ]); expect(stdout).toContain('Run instructions'); @@ -103,6 +112,8 @@ test('init --template file with custom directory', () => { projectName, '--directory', 'custom-path', + '--install-pods', + 'false', ]); // make sure --directory option is used in run instructions @@ -149,6 +160,8 @@ test('init uses npm as the package manager with --npm', () => { templatePath, 'TestInit', '--npm', + '--install-pods', + 'false', ]); expect(stdout).toContain('Run instructions'); diff --git a/__e2e__/root.test.ts b/__e2e__/root.test.ts index c59b9ae97..cb262d2a7 100644 --- a/__e2e__/root.test.ts +++ b/__e2e__/root.test.ts @@ -23,8 +23,7 @@ beforeAll(() => { writeFiles(cwd, {}); // Initialise React Native project - runCLI(cwd, ['init', 'TestProject']); - + runCLI(cwd, ['init', 'TestProject', '--install-pods']); // Link CLI to the project const pkgs = [ '@react-native-community/cli-platform-ios', diff --git a/docs/commands.md b/docs/commands.md index bef3d4e1c..eab8776ab 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -96,6 +96,10 @@ module.exports = { Skip dependencies installation +#### `--install-pods [boolean]` + +Determine if CocoaPods should be installed when initializing a project. If set to `true` it will install pods, if set to `false`, it will skip the step entirely. If not used, prompt will be displayed + #### `--npm` > [!WARNING] > `--npm` is deprecated and will be removed in the future. Please use `--pm npm` instead. diff --git a/docs/platforms.md b/docs/platforms.md index 0af5930e2..d55e9189d 100644 --- a/docs/platforms.md +++ b/docs/platforms.md @@ -111,6 +111,7 @@ On Android and iOS, this function returns a dependency configuration for: ```ts type IOSDependencyConfig = { podspecPath: string; + version: string; scriptPhases: Array; configurations: string[]; }; diff --git a/packages/cli-config/src/__tests__/__snapshots__/index-test.ts.snap b/packages/cli-config/src/__tests__/__snapshots__/index-test.ts.snap index 10dc100fd..453523b62 100644 --- a/packages/cli-config/src/__tests__/__snapshots__/index-test.ts.snap +++ b/packages/cli-config/src/__tests__/__snapshots__/index-test.ts.snap @@ -11,6 +11,7 @@ Object { ], "podspecPath": "<>/node_modules/react-native-test/ReactNativeTest.podspec", "scriptPhases": Array [], + "version": "unresolved", }, }, "root": "<>/node_modules/react-native-test", @@ -57,6 +58,7 @@ Object { "path": "./phase.sh", }, ], + "version": "unresolved", }, }, "root": "<>/node_modules/react-native-test", @@ -107,6 +109,7 @@ Object { "show_env_vars_in_log": false, }, ], + "version": "unresolved", }, }, "root": "<>/node_modules/react-native-test", @@ -131,6 +134,7 @@ Object { "configurations": Array [], "podspecPath": "<>/node_modules/react-native-test/ReactNativeTest.podspec", "scriptPhases": Array [], + "version": "unresolved", }, }, "root": "<>/node_modules/react-native-test", @@ -149,6 +153,7 @@ Object { ], "podspecPath": "<>/node_modules/react-native-test/ReactNativeTest.podspec", "scriptPhases": Array [], + "version": "unresolved", }, }, "root": "<>/node_modules/react-native-test", diff --git a/packages/cli-config/src/__tests__/index-test.ts b/packages/cli-config/src/__tests__/index-test.ts index 4ace69973..104ed7de0 100644 --- a/packages/cli-config/src/__tests__/index-test.ts +++ b/packages/cli-config/src/__tests__/index-test.ts @@ -241,6 +241,14 @@ test('supports dependencies from user configuration with custom root and propert writeFiles(DIR, { ...REACT_NATIVE_MOCK, 'native-libs/local-lib/LocalRNLibrary.podspec': '', + 'native-libs/local-lib/package.json': ` + { + "name": "local-lib", + "version": "0.0.1", + "dependencies": { + "react-native": "0.0.1" + } + }`, 'react-native.config.js': ` const path = require('path'); const root = path.resolve('${escapePathSeparator( @@ -276,6 +284,7 @@ module.exports = { "configurations": Array [], "podspecPath": "custom-path", "scriptPhases": Array [], + "version": "0.0.1", }, }, "root": "<>/native-libs/local-lib", diff --git a/packages/cli-config/src/schema.ts b/packages/cli-config/src/schema.ts index e2bbfbc1c..ee4ed7074 100644 --- a/packages/cli-config/src/schema.ts +++ b/packages/cli-config/src/schema.ts @@ -122,6 +122,7 @@ export const projectConfig = t // IOSDependencyConfig .object({ podspecPath: t.string(), + version: t.string(), configurations: t.array().items(t.string()).default([]), scriptPhases: t.array().items(t.object()).default([]), }) diff --git a/packages/cli-doctor/package.json b/packages/cli-doctor/package.json index af811e369..62bae44fb 100644 --- a/packages/cli-doctor/package.json +++ b/packages/cli-doctor/package.json @@ -24,7 +24,6 @@ "prompts": "^2.4.2", "semver": "^7.5.2", "strip-ansi": "^5.2.0", - "sudo-prompt": "^9.0.0", "wcwidth": "^1.0.1", "yaml": "^2.2.1" }, diff --git a/packages/cli-doctor/src/index.ts b/packages/cli-doctor/src/index.ts index 876158283..06e64ff66 100644 --- a/packages/cli-doctor/src/index.ts +++ b/packages/cli-doctor/src/index.ts @@ -9,4 +9,3 @@ export const commands = {info, doctor}; * refactor the init in order to remove that connection. */ export {default as versionRanges} from './tools/versionRanges'; -export {default as installPods} from './tools/installPods'; diff --git a/packages/cli-doctor/src/tools/healthchecks/cocoaPods.ts b/packages/cli-doctor/src/tools/healthchecks/cocoaPods.ts index af9057a11..64443cd4b 100644 --- a/packages/cli-doctor/src/tools/healthchecks/cocoaPods.ts +++ b/packages/cli-doctor/src/tools/healthchecks/cocoaPods.ts @@ -1,6 +1,6 @@ import execa from 'execa'; +import {runSudo} from '@react-native-community/cli-tools'; import {doesSoftwareNeedToBeFixed} from '../checkInstallation'; -import {runSudo} from '../installPods'; import {logError} from './common'; import {HealthCheckInterface} from '../../types'; import versionRanges from '../versionRanges'; diff --git a/packages/cli-platform-ios/README.md b/packages/cli-platform-ios/README.md index 4dc15d0e3..20210cd1a 100644 --- a/packages/cli-platform-ios/README.md +++ b/packages/cli-platform-ios/README.md @@ -113,6 +113,9 @@ Installs passed binary instead of building a fresh one. > default: false List all available iOS devices and simulators and let you choose one to run the app. +#### `--force-pods`, + +Force running `pod install` before running an app ### `build-ios` @@ -165,6 +168,9 @@ Example: npx react-native build-ios --extra-params "-jobs 4" ``` +#### `--force-pods`, + +Force running `pod install` before building an app ### `log-ios` Usage: diff --git a/packages/cli-platform-ios/src/__tests__/pods.test.ts b/packages/cli-platform-ios/src/__tests__/pods.test.ts new file mode 100644 index 000000000..5c5552c93 --- /dev/null +++ b/packages/cli-platform-ios/src/__tests__/pods.test.ts @@ -0,0 +1,134 @@ +import {writeFiles, getTempDirectory, cleanup} from '../../../../jest/helpers'; +import installPods from '../tools/installPods'; +import resolvePods, {compareMd5Hashes, getIosDependencies} from '../tools/pods'; + +const mockGet = jest.fn(); +const mockSet = jest.fn(); +jest.mock('@react-native-community/cli-tools', () => ({ + ...Object.assign(jest.requireActual('@react-native-community/cli-tools')), + cacheManager: { + get: mockGet, + set: mockSet, + }, +})); +jest.mock('../tools/installPods', () => jest.fn()); +const dependencyHash = 'd41d8cd98f00b204e9800998ecf8427e'; + +const packageJson = { + name: 'test-package', + dependencies: {dep1: '1.0.0'}, + devDependencies: {dep2: '1.0.0'}, +}; + +const commonDepConfig = { + root: '', + platforms: { + ios: { + podspecPath: '', + version: '1.0.0', + scriptPhases: [], + configurations: [], + }, + }, +}; + +const dependenciesConfig = { + dep1: { + name: 'dep1', + ...commonDepConfig, + }, + dep2: { + name: 'dep2', + ...commonDepConfig, + }, +}; + +const DIR = getTempDirectory('root_test'); + +const createTempFiles = (rest?: Record) => { + writeFiles(DIR, { + 'package.json': JSON.stringify(packageJson), + ...rest, + }); +}; + +beforeEach(async () => { + await cleanup(DIR); + jest.resetAllMocks(); +}); + +describe('compareMd5Hashes', () => { + it('should return false if hashes are different', () => { + const result = compareMd5Hashes('hash1', 'hash2'); + + expect(result).toBe(false); + }); + + it('should return true if hashes are the same', () => { + const result = compareMd5Hashes('hash', 'hash'); + + expect(result).toBe(true); + }); +}); + +describe('getIosDependencies', () => { + it('should return only dependencies with native code', () => { + const result = getIosDependencies(dependenciesConfig); + expect(result).toEqual(['dep1@1.0.0', 'dep2@1.0.0']); + }); +}); + +describe('resolvePods', () => { + it('should install pods if they are not installed', async () => { + createTempFiles({'ios/Podfile/Manifest.lock': ''}); + + await resolvePods(DIR, {}); + + expect(installPods).toHaveBeenCalled(); + }); + + it('should install pods when force option is set to true', async () => { + createTempFiles(); + + await resolvePods(DIR, {}, {forceInstall: true}); + + expect(installPods).toHaveBeenCalled(); + }); + + it('should install pods when there is no cached hash of dependencies', async () => { + createTempFiles(); + + await resolvePods(DIR, {}); + + expect(mockSet).toHaveBeenCalledWith( + packageJson.name, + 'dependencies', + dependencyHash, + ); + }); + + it('should skip pods installation if the cached hash and current hash are the same', async () => { + createTempFiles({'ios/Pods/Manifest.lock': ''}); + + mockGet.mockImplementation(() => dependencyHash); + + await resolvePods(DIR, {}); + + expect(installPods).not.toHaveBeenCalled(); + }); + + it('should install pods if the cached hash and current hash are different', async () => { + createTempFiles({'ios/Pods/Manifest.lock': ''}); + + mockGet.mockImplementation(() => dependencyHash); + + await resolvePods(DIR, { + dep1: { + name: 'dep1', + ...commonDepConfig, + }, + }); + + expect(installPods).toHaveBeenCalled(); + }); +}); diff --git a/packages/cli-platform-ios/src/commands/buildIOS/buildOptions.ts b/packages/cli-platform-ios/src/commands/buildIOS/buildOptions.ts index 27ebd690e..54bb00a40 100644 --- a/packages/cli-platform-ios/src/commands/buildIOS/buildOptions.ts +++ b/packages/cli-platform-ios/src/commands/buildIOS/buildOptions.ts @@ -8,6 +8,7 @@ export type BuildFlags = { interactive?: boolean; destination?: string; extraParams?: string[]; + forcePods?: boolean; }; export const buildOptions = [ diff --git a/packages/cli-platform-ios/src/commands/buildIOS/index.ts b/packages/cli-platform-ios/src/commands/buildIOS/index.ts index 08ba61b0c..1da8bb8f4 100644 --- a/packages/cli-platform-ios/src/commands/buildIOS/index.ts +++ b/packages/cli-platform-ios/src/commands/buildIOS/index.ts @@ -11,10 +11,14 @@ import {buildProject} from './buildProject'; import {BuildFlags, buildOptions} from './buildOptions'; import {getConfiguration} from './getConfiguration'; import {getXcodeProjectAndDir} from './getXcodeProjectAndDir'; +import resolvePods from '../../tools/pods'; async function buildIOS(_: Array, ctx: Config, args: BuildFlags) { const {xcodeProject, sourceDir} = getXcodeProjectAndDir(ctx.project.ios); + // check if pods need to be installed + await resolvePods(ctx.root, ctx.dependencies, {forceInstall: args.forcePods}); + process.chdir(sourceDir); const {scheme, mode} = await getConfiguration(xcodeProject, sourceDir, args); diff --git a/packages/cli-platform-ios/src/commands/runIOS/index.ts b/packages/cli-platform-ios/src/commands/runIOS/index.ts index eb0b95244..30f584301 100644 --- a/packages/cli-platform-ios/src/commands/runIOS/index.ts +++ b/packages/cli-platform-ios/src/commands/runIOS/index.ts @@ -30,6 +30,7 @@ import listIOSDevices from '../../tools/listIOSDevices'; import {promptForDeviceSelection} from '../../tools/prompts'; import getSimulators from '../../tools/getSimulators'; import {getXcodeProjectAndDir} from '../buildIOS/getXcodeProjectAndDir'; +import resolvePods from '../../tools/pods'; export interface FlagsT extends BuildFlags { simulator?: string; @@ -47,6 +48,9 @@ async function runIOS(_: Array, ctx: Config, args: FlagsT) { let {packager, port} = args; + // check if pods need to be installed + await resolvePods(ctx.root, ctx.dependencies, {forceInstall: args.forcePods}); + const packagerStatus = await isPackagerRunning(port); if ( diff --git a/packages/cli-platform-ios/src/config/index.ts b/packages/cli-platform-ios/src/config/index.ts index d54e6cab9..0232a4874 100644 --- a/packages/cli-platform-ios/src/config/index.ts +++ b/packages/cli-platform-ios/src/config/index.ts @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. * */ +import chalk from 'chalk'; import path from 'path'; import fs from 'fs'; import findPodfilePath from './findPodfilePath'; @@ -17,6 +18,7 @@ import { IOSProjectConfig, IOSDependencyConfig, } from '@react-native-community/cli-types'; +import {CLIError} from '@react-native-community/cli-tools'; /** * Returns project config by analyzing given folder and applying some user defaults @@ -66,8 +68,25 @@ export function dependencyConfig( return null; } + let version = 'unresolved'; + + try { + const packageJson = require(path.join(folder, 'package.json')); + + if (packageJson.version) { + version = packageJson.version; + } + } catch { + throw new CLIError( + `Failed to locate package.json file from ${chalk.underline( + folder, + )}. This is most likely issue with your node_modules folder being corrupted. Please force install dependencies and try again`, + ); + } + return { podspecPath, + version, configurations: userConfig.configurations || [], scriptPhases: userConfig.scriptPhases || [], }; diff --git a/packages/cli-platform-ios/src/index.ts b/packages/cli-platform-ios/src/index.ts index a612686d2..5242bb9fe 100644 --- a/packages/cli-platform-ios/src/index.ts +++ b/packages/cli-platform-ios/src/index.ts @@ -4,3 +4,4 @@ export {default as commands} from './commands'; export {projectConfig, dependencyConfig, findPodfilePaths} from './config'; +export {default as installPods} from './tools/installPods'; diff --git a/packages/cli-doctor/src/tools/installPods.ts b/packages/cli-platform-ios/src/tools/installPods.ts similarity index 86% rename from packages/cli-doctor/src/tools/installPods.ts rename to packages/cli-platform-ios/src/tools/installPods.ts index 3f531f64d..08ecadb93 100644 --- a/packages/cli-doctor/src/tools/installPods.ts +++ b/packages/cli-platform-ios/src/tools/installPods.ts @@ -1,18 +1,22 @@ import fs from 'fs'; import execa from 'execa'; +import type {Ora} from 'ora'; import chalk from 'chalk'; import { logger, NoopLoader, link, CLIError, + runSudo, } from '@react-native-community/cli-tools'; -import sudo from 'sudo-prompt'; import runBundleInstall from './runBundleInstall'; -import {Loader} from '../types'; + +interface PodInstallOptions { + skipBundleInstall?: boolean; +} async function runPodInstall( - loader: Loader, + loader: Ora, shouldHandleRepoUpdate: boolean = true, ) { try { @@ -52,7 +56,7 @@ async function runPodInstall( } } -async function runPodUpdate(loader: Loader) { +async function runPodUpdate(loader: Ora) { try { loader.start( `Updating CocoaPods repositories ${chalk.dim( @@ -73,18 +77,6 @@ async function runPodUpdate(loader: Loader) { } } -function runSudo(command: string): Promise { - return new Promise((resolve, reject) => { - sudo.exec(command, {name: 'React Native CLI'}, (error) => { - if (error) { - reject(error); - } - - resolve(); - }); - }); -} - async function installCocoaPodsWithGem() { const options = ['install', 'cocoapods', '--no-document']; @@ -97,7 +89,7 @@ async function installCocoaPodsWithGem() { } } -async function installCocoaPods(loader: Loader) { +async function installCocoaPods(loader: Ora) { loader.stop(); loader.start('Installing CocoaPods'); @@ -118,7 +110,7 @@ async function installCocoaPods(loader: Loader) { } } -async function installPods(loader?: Loader) { +async function installPods(loader?: Ora, options?: PodInstallOptions) { loader = loader || new NoopLoader(); try { if (!fs.existsSync('ios')) { @@ -133,7 +125,7 @@ async function installPods(loader?: Loader) { return; } - if (fs.existsSync('../Gemfile')) { + if (fs.existsSync('../Gemfile') && !options?.skipBundleInstall) { await runBundleInstall(loader); } @@ -153,6 +145,6 @@ async function installPods(loader?: Loader) { } } -export {runSudo, installCocoaPods}; +export {installCocoaPods}; export default installPods; diff --git a/packages/cli-platform-ios/src/tools/pods.ts b/packages/cli-platform-ios/src/tools/pods.ts new file mode 100644 index 000000000..cb56f1085 --- /dev/null +++ b/packages/cli-platform-ios/src/tools/pods.ts @@ -0,0 +1,104 @@ +import path from 'path'; +import fs from 'fs-extra'; +import {createHash} from 'crypto'; +import chalk from 'chalk'; +import { + CLIError, + cacheManager, + getLoader, +} from '@react-native-community/cli-tools'; +import installPods from './installPods'; +import findPodfilePath from '../config/findPodfilePath'; +import { + DependencyConfig, + IOSDependencyConfig, +} from '@react-native-community/cli-types'; + +interface ResolvePodsOptions { + forceInstall?: boolean; +} + +interface NativeDependencies { + [key: string]: DependencyConfig; +} + +export function getPackageJson(root: string) { + try { + return require(path.join(root, 'package.json')); + } catch { + throw new CLIError( + 'No package.json found. Please make sure the file exists in the current folder.', + ); + } +} + +export function getIosDependencies(dependencies: NativeDependencies) { + return Object.keys(dependencies) + .filter((dependency) => dependencies[dependency].platforms.ios) + .map( + (dependency) => + `${dependency}@${ + (dependencies[dependency].platforms.ios as IOSDependencyConfig) + .version + }`, + ) + .sort(); +} + +export function dependenciesToString(dependencies: string[]) { + return dependencies.join('\n'); +} + +export function generateMd5Hash(text: string) { + return createHash('md5').update(text).digest('hex'); +} + +export function compareMd5Hashes(hash1: string, hash2: string) { + return hash1 === hash2; +} + +export default async function resolvePods( + root: string, + nativeDependencies: NativeDependencies, + options?: ResolvePodsOptions, +) { + const packageJson = getPackageJson(root); + const podfilePath = findPodfilePath(root); + const iosFolderPath = podfilePath + ? podfilePath.slice(0, podfilePath.lastIndexOf('/')) + : path.join(root, 'ios'); + const podsPath = path.join(iosFolderPath, 'Pods'); + const arePodsInstalled = fs.existsSync(podsPath); + const iosDependencies = getIosDependencies(nativeDependencies); + const dependenciesString = dependenciesToString(iosDependencies); + const currentDependenciesHash = generateMd5Hash(dependenciesString); + const cachedDependenciesHash = cacheManager.get( + packageJson.name, + 'dependencies', + ); + + if ( + !cachedDependenciesHash || + !compareMd5Hashes(currentDependenciesHash, cachedDependenciesHash) || + !arePodsInstalled || + options?.forceInstall + ) { + const loader = getLoader('Installing CocoaPods...'); + try { + await installPods(loader, {skipBundleInstall: !!cachedDependenciesHash}); + cacheManager.set( + packageJson.name, + 'dependencies', + currentDependenciesHash, + ); + loader.succeed(); + } catch { + loader.fail(); + throw new CLIError( + `Something when wrong while installing CocoaPods. Please run ${chalk.bold( + 'pod install', + )} manually`, + ); + } + } +} diff --git a/packages/cli-doctor/src/tools/runBundleInstall.ts b/packages/cli-platform-ios/src/tools/runBundleInstall.ts similarity index 88% rename from packages/cli-doctor/src/tools/runBundleInstall.ts rename to packages/cli-platform-ios/src/tools/runBundleInstall.ts index ce601528f..ca46bfa1c 100644 --- a/packages/cli-doctor/src/tools/runBundleInstall.ts +++ b/packages/cli-platform-ios/src/tools/runBundleInstall.ts @@ -1,9 +1,8 @@ import execa from 'execa'; import {CLIError, logger, link} from '@react-native-community/cli-tools'; +import type {Ora} from 'ora'; -import {Loader} from '../types'; - -async function runBundleInstall(loader: Loader) { +async function runBundleInstall(loader: Ora) { try { loader.start('Installing Ruby Gems'); diff --git a/packages/cli-tools/package.json b/packages/cli-tools/package.json index 4403f8855..b69e474cb 100644 --- a/packages/cli-tools/package.json +++ b/packages/cli-tools/package.json @@ -15,7 +15,8 @@ "open": "^6.2.0", "ora": "^5.4.1", "semver": "^7.5.2", - "shell-quote": "^1.7.3" + "shell-quote": "^1.7.3", + "sudo-prompt": "^9.0.0" }, "devDependencies": { "@react-native-community/cli-types": "12.0.0-alpha.13", diff --git a/packages/cli-tools/src/releaseChecker/releaseCacheManager.ts b/packages/cli-tools/src/cacheManager.ts similarity index 79% rename from packages/cli-tools/src/releaseChecker/releaseCacheManager.ts rename to packages/cli-tools/src/cacheManager.ts index 08cae4161..367d7b513 100644 --- a/packages/cli-tools/src/releaseChecker/releaseCacheManager.ts +++ b/packages/cli-tools/src/cacheManager.ts @@ -2,10 +2,10 @@ import path from 'path'; import fs from 'fs'; import os from 'os'; import appDirs from 'appdirsjs'; -import logger from '../logger'; +import logger from './logger'; -type ReleaseCacheKey = 'eTag' | 'lastChecked' | 'latestVersion'; -type Cache = {[key in ReleaseCacheKey]?: string}; +type CacheKey = 'eTag' | 'lastChecked' | 'latestVersion' | 'dependencies'; +type Cache = {[key in CacheKey]?: string}; function loadCache(name: string): Cache | undefined { try { @@ -20,7 +20,7 @@ function loadCache(name: string): Cache | undefined { // Create cache file since it doesn't exist. saveCache(name, {}); } - logger.debug('No release cache found'); + logger.debug('No cache found'); return undefined; } } @@ -48,7 +48,7 @@ function getCacheRootPath() { return cachePath; } -function get(name: string, key: ReleaseCacheKey): string | undefined { +function get(name: string, key: CacheKey): string | undefined { const cache = loadCache(name); if (cache) { return cache[key]; @@ -56,7 +56,7 @@ function get(name: string, key: ReleaseCacheKey): string | undefined { return undefined; } -function set(name: string, key: ReleaseCacheKey, value: string) { +function set(name: string, key: CacheKey, value: string) { const cache = loadCache(name); if (cache) { cache[key] = value; diff --git a/packages/cli-tools/src/index.ts b/packages/cli-tools/src/index.ts index 5628d5265..f25fcdb86 100644 --- a/packages/cli-tools/src/index.ts +++ b/packages/cli-tools/src/index.ts @@ -14,5 +14,7 @@ export * as link from './doclink'; export {default as startServerInNewWindow} from './startServerInNewWindow'; export {default as handlePortUnavailable} from './handlePortUnavailable'; export * from './port'; +export {default as cacheManager} from './cacheManager'; +export {default as runSudo} from './runSudo'; export * from './errors'; diff --git a/packages/cli-tools/src/releaseChecker/getLatestRelease.ts b/packages/cli-tools/src/releaseChecker/getLatestRelease.ts index 7b438cb3f..e9989f223 100644 --- a/packages/cli-tools/src/releaseChecker/getLatestRelease.ts +++ b/packages/cli-tools/src/releaseChecker/getLatestRelease.ts @@ -1,5 +1,5 @@ import semver from 'semver'; -import cacheManager from './releaseCacheManager'; +import cacheManager from '../cacheManager'; import {fetch} from '../fetch'; import logger from '../logger'; diff --git a/packages/cli-tools/src/releaseChecker/printNewRelease.ts b/packages/cli-tools/src/releaseChecker/printNewRelease.ts index 4653b9f12..d9f13e429 100644 --- a/packages/cli-tools/src/releaseChecker/printNewRelease.ts +++ b/packages/cli-tools/src/releaseChecker/printNewRelease.ts @@ -4,7 +4,7 @@ import * as link from '../doclink'; import logger from '../logger'; import {Release} from './getLatestRelease'; -import cacheManager from './releaseCacheManager'; +import cacheManager from '../cacheManager'; /** * Notifies the user that a newer version of React Native is available. diff --git a/packages/cli-tools/src/runSudo.ts b/packages/cli-tools/src/runSudo.ts new file mode 100644 index 000000000..eb406783c --- /dev/null +++ b/packages/cli-tools/src/runSudo.ts @@ -0,0 +1,13 @@ +import sudo from 'sudo-prompt'; + +export default function runSudo(command: string): Promise { + return new Promise((resolve, reject) => { + sudo.exec(command, {name: 'React Native CLI'}, (error) => { + if (error) { + reject(error); + } + + resolve(); + }); + }); +} diff --git a/packages/cli-types/src/ios.ts b/packages/cli-types/src/ios.ts index 3c0b54163..808791d44 100644 --- a/packages/cli-types/src/ios.ts +++ b/packages/cli-types/src/ios.ts @@ -21,13 +21,14 @@ export interface IOSProjectConfig { export interface IOSDependencyConfig { podspecPath: string; + version: string; scriptPhases: Array; configurations: string[]; } export type IOSDependencyParams = Omit< Partial, - 'podspecPath' + 'podspecPath' | 'version' >; /** diff --git a/packages/cli/src/commands/init/index.ts b/packages/cli/src/commands/init/index.ts index af773e319..be2aaee2f 100644 --- a/packages/cli/src/commands/init/index.ts +++ b/packages/cli/src/commands/init/index.ts @@ -37,6 +37,11 @@ export default { name: '--skip-install', description: 'Skips dependencies installation step', }, + { + name: '--install-pods [boolean]', + description: + 'Determine if CocoaPods should be installed when initializing a project', + }, { name: '--package-name ', description: diff --git a/packages/cli/src/commands/init/init.ts b/packages/cli/src/commands/init/init.ts index 30ad44c1f..b6351cb19 100644 --- a/packages/cli/src/commands/init/init.ts +++ b/packages/cli/src/commands/init/init.ts @@ -2,6 +2,8 @@ import os from 'os'; import path from 'path'; import fs from 'fs-extra'; import {validateProjectName} from './validate'; +import {prompt} from 'prompts'; +import chalk from 'chalk'; import DirectoryAlreadyExistsError from './errors/DirectoryAlreadyExistsError'; import printRunInstructions from './printRunInstructions'; import { @@ -9,7 +11,9 @@ import { logger, getLoader, Loader, + cacheManager, } from '@react-native-community/cli-tools'; +import {installPods} from '@react-native-community/cli-platform-ios'; import { installTemplatePackage, getTemplateConfig, @@ -18,13 +22,12 @@ import { } from './template'; import {changePlaceholderInTemplate} from './editTemplate'; import * as PackageManager from '../../tools/packageManager'; -import {installPods} from '@react-native-community/cli-doctor'; import banner from './banner'; import TemplateAndVersionError from './errors/TemplateAndVersionError'; import {getBunVersionIfAvailable} from '../../tools/bun'; import {getNpmVersionIfAvailable} from '../../tools/npm'; import {getYarnVersionIfAvailable} from '../../tools/yarn'; -import prompts from 'prompts'; +import {createHash} from 'crypto'; const DEFAULT_VERSION = 'latest'; @@ -38,6 +41,7 @@ type Options = { skipInstall?: boolean; version?: string; packageName?: string; + installPods?: string | boolean; }; interface TemplateOptions { @@ -49,6 +53,7 @@ interface TemplateOptions { projectTitle?: string; skipInstall?: boolean; packageName?: string; + installCocoaPods?: string | boolean; } function doesDirectoryExist(dir: string) { @@ -84,6 +89,15 @@ function getTemplateName(cwd: string) { return name; } +//set cache to empty string to prevent installing cocoapods on freshly created project +function setEmptyHashForCachedDependencies(projectName: string) { + cacheManager.set( + projectName, + 'dependencies', + createHash('md5').update('').digest('hex'), + ); +} + async function createFromTemplate({ projectName, templateUri, @@ -93,6 +107,7 @@ async function createFromTemplate({ projectTitle, skipInstall, packageName, + installCocoaPods, }: TemplateOptions) { logger.debug('Initializing new project'); logger.log(banner); @@ -170,6 +185,30 @@ async function createFromTemplate({ loader, root: projectDirectory, }); + + if (process.platform === 'darwin') { + const installPodsValue = String(installCocoaPods); + + if (installPodsValue === 'true') { + await installPods(loader); + loader.succeed(); + setEmptyHashForCachedDependencies(projectName); + } else if (installPodsValue === 'undefined') { + const {installCocoapods} = await prompt({ + type: 'confirm', + name: 'installCocoapods', + message: `Do you want to install CocoaPods now? ${chalk.reset.dim( + 'Only needed if you run your project in Xcode directly', + )}`, + }); + + if (installCocoapods) { + await installPods(loader); + loader.succeed(); + setEmptyHashForCachedDependencies(projectName); + } + } + } } else { loader.succeed('Dependencies installation skipped'); } @@ -198,10 +237,6 @@ async function installDependencies({ root, }); - if (process.platform === 'darwin') { - await installPods(loader); - } - loader.succeed(); } @@ -250,6 +285,7 @@ async function createProject( projectTitle: options.title, skipInstall: options.skipInstall, packageName: options.packageName, + installCocoaPods: options.installPods, }); } @@ -276,7 +312,7 @@ export default (async function initialize( options: Options, ) { if (!projectName) { - const {projName} = await prompts({ + const {projName} = await prompt({ type: 'text', name: 'projName', message: 'How would you like to name the app?',