diff --git a/CHANGELOG.md b/CHANGELOG.md index 55ade56f86..61bd6f00b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ This is the log of notable changes to EAS CLI and related packages. ### ๐Ÿ› Bug fixes - eas init should fix and validate project name and slug. ([#3277](https://github.com/expo/eas-cli/pull/3277) by [@douglowder](https://github.com/douglowder)) +- Prevent `npx` invocations that can be unreliable and fail when retrieving entitlements or project configs ([#3282](https://github.com/expo/eas-cli/pull/3282) by [@kitten](https://github.com/kitten)) ### ๐Ÿงน Chores diff --git a/packages/eas-cli/src/commandUtils/context/contextUtils/__tests__/getProjectIdAsync-test.ts b/packages/eas-cli/src/commandUtils/context/contextUtils/__tests__/getProjectIdAsync-test.ts index d8380c3fef..4588021d65 100644 --- a/packages/eas-cli/src/commandUtils/context/contextUtils/__tests__/getProjectIdAsync-test.ts +++ b/packages/eas-cli/src/commandUtils/context/contextUtils/__tests__/getProjectIdAsync-test.ts @@ -73,7 +73,10 @@ describe(getProjectIdAsync, () => { ); jest.mocked(findProjectRootAsync).mockResolvedValue('/app'); - jest.mocked(isExpoInstalled).mockReturnValue(true); + + // NOTE(@kitten): Updating this test is easiest by letting it fallback to `@expo/config` + // This isn't a great solution, but the test is pretty involved + jest.mocked(isExpoInstalled).mockReturnValue(false); }); it('gets the project ID from app config if exists', async () => { diff --git a/packages/eas-cli/src/commandUtils/new/__tests__/commands.test.ts b/packages/eas-cli/src/commandUtils/new/__tests__/commands.test.ts index 89d1fe7ea9..e11612ef08 100644 --- a/packages/eas-cli/src/commandUtils/new/__tests__/commands.test.ts +++ b/packages/eas-cli/src/commandUtils/new/__tests__/commands.test.ts @@ -1,12 +1,14 @@ import { canAccessRepositoryUsingSshAsync, runGitCloneAsync } from '../../../onboarding/git'; import { installDependenciesAsync } from '../../../onboarding/installDependencies'; import { runCommandAsync } from '../../../onboarding/runCommand'; +import { expoCommandAsync } from '../../../utils/expoCli'; import { cloneTemplateAsync, initializeGitRepositoryAsync, installProjectDependenciesAsync, } from '../commands'; +jest.mock('../../../utils/expoCli'); jest.mock('../../../onboarding/git'); jest.mock('../../../onboarding/runCommand'); jest.mock('../../../onboarding/installDependencies'); @@ -79,21 +81,11 @@ describe('commands', () => { packageManager: 'npm', }); - expect(runCommandAsync).toHaveBeenCalledWith({ - cwd: projectDir, - command: 'npx', - args: ['expo', 'install', 'expo-updates'], - showOutput: false, - showSpinner: false, - }); - - expect(runCommandAsync).toHaveBeenCalledWith({ - cwd: projectDir, - command: 'npx', - args: ['expo', 'install', '@expo/metro-runtime'], - showOutput: false, - showSpinner: false, - }); + expect(expoCommandAsync).toHaveBeenCalledWith( + projectDir, + ['install', 'expo-updates', '@expo/metro-runtime'], + { silent: true } + ); }); }); diff --git a/packages/eas-cli/src/commandUtils/new/commands.ts b/packages/eas-cli/src/commandUtils/new/commands.ts index e90762150a..cdda28f0c6 100644 --- a/packages/eas-cli/src/commandUtils/new/commands.ts +++ b/packages/eas-cli/src/commandUtils/new/commands.ts @@ -7,6 +7,7 @@ import { canAccessRepositoryUsingSshAsync, runGitCloneAsync } from '../../onboar import { PackageManager, installDependenciesAsync } from '../../onboarding/installDependencies'; import { runCommandAsync } from '../../onboarding/runCommand'; import { ora } from '../../ora'; +import { expoCommandAsync } from '../../utils/expoCli'; export async function cloneTemplateAsync(targetProjectDir: string): Promise { const githubUsername = 'expo'; @@ -48,16 +49,8 @@ export async function installProjectDependenciesAsync( }); const dependencies = ['expo-updates', '@expo/metro-runtime']; - for (const dependency of dependencies) { - spinner.text = `Installing ${chalk.bold(dependency)}`; - await runCommandAsync({ - cwd: projectDir, - command: 'npx', - args: ['expo', 'install', dependency], - showOutput: false, - showSpinner: false, - }); - } + spinner.text = `Installing ${dependencies.map(dep => chalk.bold(dep)).join(', ')}`; + await expoCommandAsync(projectDir, ['install', ...dependencies], { silent: true }); spinner.succeed(`Installed project dependencies`); } diff --git a/packages/eas-cli/src/commands/project/__tests__/init.test.ts b/packages/eas-cli/src/commands/project/__tests__/init.test.ts index ebf81566eb..b6db0777d7 100644 --- a/packages/eas-cli/src/commands/project/__tests__/init.test.ts +++ b/packages/eas-cli/src/commands/project/__tests__/init.test.ts @@ -94,7 +94,10 @@ function mockTestProject(options: { graphqlClient, authenticationInfo: { accessToken: null, sessionSecret: '1234' }, }); - jest.mocked(isExpoInstalled).mockReturnValue(true); + + // NOTE(@kitten): Updating this test is easiest by letting it fallback to `@expo/config` + // This isn't a great solution, but the test is pretty involved + jest.mocked(isExpoInstalled).mockReturnValue(false); } const commandOptions = { root: '/test-project' } as any; diff --git a/packages/eas-cli/src/commands/project/onboarding.ts b/packages/eas-cli/src/commands/project/onboarding.ts index 828480c67d..251a00cb5d 100644 --- a/packages/eas-cli/src/commands/project/onboarding.ts +++ b/packages/eas-cli/src/commands/project/onboarding.ts @@ -35,6 +35,7 @@ import { ExpoConfigOptions, getPrivateExpoConfigAsync } from '../../project/expo import { promptAsync } from '../../prompts'; import { Actor } from '../../user/User'; import { easCliVersion } from '../../utils/easCli'; +import { expoCommandAsync } from '../../utils/expoCli'; import GitClient from '../../vcs/clients/git'; export default class Onboarding extends EasCommand { @@ -196,23 +197,12 @@ export default class Onboarding extends EasCommand { }); if (!app.githubRepository) { - await runCommandAsync({ - cwd: finalTargetProjectDirectory, - command: 'npx', - args: ['expo', 'install', 'expo-updates'], - }); - Log.log(); - await runCommandAsync({ - cwd: finalTargetProjectDirectory, - command: 'npx', - args: ['expo', 'install', 'expo-insights'], - }); - Log.log(); - await runCommandAsync({ - cwd: finalTargetProjectDirectory, - command: 'npx', - args: ['expo', 'install', 'expo-dev-client'], - }); + await expoCommandAsync(finalTargetProjectDirectory, [ + 'install', + 'expo-updates', + 'expo-insights', + 'expo-dev-client', + ]); Log.log(); } await vcsClient.trackFileAsync('package-lock.json'); diff --git a/packages/eas-cli/src/project/expoConfig.ts b/packages/eas-cli/src/project/expoConfig.ts index d44f00cfcc..e1fec4493c 100644 --- a/packages/eas-cli/src/project/expoConfig.ts +++ b/packages/eas-cli/src/project/expoConfig.ts @@ -1,12 +1,11 @@ import { ExpoConfig, getConfig, getConfigFilePaths, modifyConfigAsync } from '@expo/config'; import { Env } from '@expo/eas-build-job'; -import spawnAsync from '@expo/spawn-async'; import fs from 'fs-extra'; import Joi from 'joi'; import path from 'path'; import { isExpoInstalled } from './projectUtils'; -import Log from '../log'; +import { spawnExpoCommand } from '../utils/expoCli'; export type PublicExpoConfig = Omit< ExpoConfig, @@ -41,8 +40,6 @@ export async function createOrModifyExpoConfigAsync( } } -let wasExpoConfigWarnPrinted = false; - async function getExpoConfigInternalAsync( projectDir: string, opts: ExpoConfigOptionsInternal = {} @@ -56,35 +53,16 @@ async function getExpoConfigInternalAsync( let exp: ExpoConfig; if (isExpoInstalled(projectDir)) { - try { - const { stdout } = await spawnAsync( - 'npx', - ['expo', 'config', '--json', ...(opts.isPublicConfig ? ['--type', 'public'] : [])], - - { - cwd: projectDir, - env: { - ...process.env, - ...opts.env, - EXPO_NO_DOTENV: '1', - }, - } - ); - exp = JSON.parse(stdout); - } catch (err: any) { - if (!wasExpoConfigWarnPrinted) { - Log.warn( - `Failed to read the app config from the project using "npx expo config" command: ${err.message}.` - ); - Log.warn('Falling back to the version of "@expo/config" shipped with the EAS CLI.'); - wasExpoConfigWarnPrinted = true; + const { stdout } = await spawnExpoCommand( + projectDir, + ['config', '--json', ...(opts.isPublicConfig ? ['--type', 'public'] : [])], + { + env: { + EXPO_NO_DOTENV: '1', + }, } - exp = getConfig(projectDir, { - skipSDKVersionRequirement: true, - ...(opts.isPublicConfig ? { isPublicConfig: true } : {}), - ...(opts.skipPlugins ? { skipPlugins: true } : {}), - }).exp; - } + ); + exp = JSON.parse(stdout); } else { exp = getConfig(projectDir, { skipSDKVersionRequirement: true, diff --git a/packages/eas-cli/src/project/ios/entitlements.ts b/packages/eas-cli/src/project/ios/entitlements.ts index 918990347b..b4dcaa85f5 100644 --- a/packages/eas-cli/src/project/ios/entitlements.ts +++ b/packages/eas-cli/src/project/ios/entitlements.ts @@ -1,69 +1,27 @@ -import { ExportedConfig, IOSConfig, compileModsAsync } from '@expo/config-plugins'; +import { ExportedConfig, IOSConfig } from '@expo/config-plugins'; import { JSONObject } from '@expo/json-file'; -import { getPrebuildConfigAsync } from '@expo/prebuild-config'; -import spawnAsync from '@expo/spawn-async'; -import Log from '../../log'; +import { spawnExpoCommand } from '../../utils/expoCli'; import { readPlistAsync } from '../../utils/plist'; -import { Client } from '../../vcs/vcs'; -import { hasIgnoredIosProjectAsync } from '../workflow'; interface Target { buildConfiguration?: string; targetName: string; } -let wasExpoConfigPluginsWarnPrinted = false; - export async function getManagedApplicationTargetEntitlementsAsync( projectDir: string, - env: Record, - vcsClient: Client + env: Record ): Promise { - const originalProcessEnv: NodeJS.ProcessEnv = process.env; - - try { - process.env = { - ...process.env, - ...env, - }; - - let expWithMods: ExportedConfig; - try { - const { stdout } = await spawnAsync( - 'npx', - ['expo', 'config', '--json', '--type', 'introspect'], - - { - cwd: projectDir, - env: { - ...process.env, - ...env, - EXPO_NO_DOTENV: '1', - }, - } - ); - expWithMods = JSON.parse(stdout); - } catch (err: any) { - if (!wasExpoConfigPluginsWarnPrinted) { - Log.warn( - `Failed to read the app config from the project using "npx expo config" command: ${err.message}.` - ); - Log.warn('Falling back to the version of "@expo/config" shipped with the EAS CLI.'); - wasExpoConfigPluginsWarnPrinted = true; - } - const { exp } = await getPrebuildConfigAsync(projectDir, { platforms: ['ios'] }); - expWithMods = await compileModsAsync(exp, { - projectRoot: projectDir, - platforms: ['ios'], - introspect: true, - ignoreExistingNativeFiles: await hasIgnoredIosProjectAsync(projectDir, vcsClient), - }); + const { stdout } = await spawnExpoCommand( + projectDir, + ['config', '--json', '--type', 'introspect'], + { + env, } - return expWithMods.ios?.entitlements ?? {}; - } finally { - process.env = originalProcessEnv; - } + ); + const expWithMods: ExportedConfig = JSON.parse(stdout); + return expWithMods.ios?.entitlements ?? {}; } export async function getNativeTargetEntitlementsAsync( diff --git a/packages/eas-cli/src/project/ios/target.ts b/packages/eas-cli/src/project/ios/target.ts index f8ff5446a7..5008efaa7c 100644 --- a/packages/eas-cli/src/project/ios/target.ts +++ b/packages/eas-cli/src/project/ios/target.ts @@ -60,8 +60,7 @@ export async function resolveManagedProjectTargetsAsync({ ); const applicationTargetEntitlements = await getManagedApplicationTargetEntitlementsAsync( projectDir, - env ?? {}, - vcsClient + env ?? {} ); const appExtensions: UserDefinedTarget[] = exp.extra?.eas?.build?.experimental?.ios?.appExtensions ?? []; diff --git a/packages/eas-cli/src/utils/expoCli.ts b/packages/eas-cli/src/utils/expoCli.ts index 6c1e6fda00..9d006dcb22 100644 --- a/packages/eas-cli/src/utils/expoCli.ts +++ b/packages/eas-cli/src/utils/expoCli.ts @@ -2,6 +2,7 @@ import { ExpoConfig } from '@expo/config'; import spawnAsync from '@expo/spawn-async'; import chalk from 'chalk'; import { boolish } from 'getenv'; +import type { CommonSpawnOptions } from 'node:child_process'; import resolveFrom, { silent as silentResolveFrom } from 'resolve-from'; import semver from 'semver'; @@ -77,14 +78,11 @@ export const shouldUseVersionedExpoCLIWithExplicitPlatforms = memoize( shouldUseVersionedExpoCLIWithExplicitPlatformsExpensive ); -export async function expoCommandAsync( +export function spawnExpoCommand( projectDir: string, args: string[], - { - silent = false, - extraEnv = {}, - }: { silent?: boolean; extraEnv?: Record } = {} -): Promise { + opts?: CommonSpawnOptions +): spawnAsync.SpawnPromise { let expoCliPath; try { expoCliPath = @@ -101,25 +99,39 @@ export async function expoCommandAsync( } const spawnPromise = spawnAsync(expoCliPath, args, { - stdio: ['inherit', 'pipe', 'pipe'], // inherit stdin so user can install a missing expo-cli from inside this command + cwd: projectDir, + ...opts, env: { ...process.env, - ...extraEnv, + ...opts?.env, }, }); - const { - child: { stdout, stderr }, - } = spawnPromise; - if (!(stdout && stderr)) { + if (!spawnPromise.child.stdout && !spawnPromise.child.stderr) { throw new Error('Failed to spawn expo-cli'); } + + return spawnPromise; +} + +export async function expoCommandAsync( + projectDir: string, + args: string[], + { + silent = false, + extraEnv = {}, + }: { silent?: boolean; extraEnv?: Record } = {} +): Promise { + const spawnPromise = spawnExpoCommand(projectDir, args, { + stdio: ['inherit', 'pipe', 'pipe'], // inherit stdin so user can install a missing expo-cli from inside this command + env: extraEnv, + }); if (!silent) { - stdout.on('data', data => { + spawnPromise.child.stdout?.on('data', data => { for (const line of data.toString().trim().split('\n')) { Log.log(`${chalk.gray('[expo-cli]')} ${line}`); } }); - stderr.on('data', data => { + spawnPromise.child.stderr?.on('data', data => { for (const line of data.toString().trim().split('\n')) { Log.warn(`${chalk.gray('[expo-cli]')} ${line}`); }