Skip to content

Commit

Permalink
[eas-cli] use npx expo config command to resolve expo config if ava…
Browse files Browse the repository at this point in the history
…ilable (#2529)

<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->

# Why

https://exponent-internal.slack.com/archives/C02123T524U/p1724970663050089?thread_ts=1724924614.773089&cid=C02123T524U
https://exponent-internal.slack.com/archives/C5ERY0TAR/p1725002816077089

Generally `@expo/config`, `@expo/prebuild-config`, and `@expo/config-plugins` are per SDK concept. Currently, in EAS CLI, we rely on one version of the `@expo/...` package to work well globally for all SDKs. It seems to sometimes bite us like in the case of https://exponent-internal.slack.com/archives/C02123T524U/p1724909908574979, because SDK 51 config wasn't compatible with SDK 50.

It's better to switch to using config-related functions shipped with `@expo/config`, `@expo/prebuild-config`, and `@expo/config-plugins`.  I believe we can assume that `getConfig`, `getPrebuildConfig`, and `compileModsAsync` shipped with these packages have stable API https://exponent-internal.slack.com/archives/C5ERY0TAR/p1725293002128849?thread_ts=1725002816.077089&cid=C5ERY0TAR.

# How

Use `getConfig`, `getPrebuildConfig`, and `compileModsAsync` from `@expo/config`, `@expo/prebuild-config`, and `@expo/config-plugins` shipped with Expo SDK installed in the user's project. If not available, fallback to versions installed per SDK. If we can assume that these guarantee stable API we can use types of these functions from packages installed for EAS CLI as well.

# Test Plan

Run builds for repro case of https://exponent-internal.slack.com/archives/C02123T524U/p1724909908574979 with SDK 51 config packages installed for EAS CLI (that previously broke it), see that it works and capabilities are correctly synced.

Initiate a new bare RN project using community CLI, and check that it works as well using fallback config loading.
  • Loading branch information
szdziedzic authored Nov 5, 2024
1 parent cf8e715 commit ac799bb
Show file tree
Hide file tree
Showing 23 changed files with 160 additions and 91 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ This is the log of notable changes to EAS CLI and related packages.

### 🛠 Breaking changes

- Resolve versioned expo config using `npx expo config` command instead of using fixed `@expo/config` version shipped with EAS CLI, if available. ([#2529](https://github.com/expo/eas-cli/pull/2529) by [@szdziedzic](https://github.com/szdziedzic))

### 🎉 New features

### 🐛 Bug fixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
import { loadServerSideEnvironmentVariablesAsync } from './contextUtils/loadServerSideEnvironmentVariablesAsync';
import {
ExpoConfigOptions,
getPrivateExpoConfig,
getPublicExpoConfig,
getPrivateExpoConfigAsync,
getPublicExpoConfigAsync,
} from '../../project/expoConfig';

export type DynamicConfigContextFn = (options?: ExpoConfigOptions) => Promise<{
Expand All @@ -25,7 +25,7 @@ export class DynamicPublicProjectConfigContextField extends ContextField<Dynamic
}: ContextOptions): Promise<DynamicConfigContextFn> {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
return async (options?: ExpoConfigOptions) => {
const expBefore = getPublicExpoConfig(projectDir, options);
const expBefore = await getPublicExpoConfigAsync(projectDir, options);
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive,
env: options?.env,
Expand All @@ -48,7 +48,7 @@ export class DynamicPublicProjectConfigContextField extends ContextField<Dynamic
},
};
}
const exp = getPublicExpoConfig(projectDir, options);
const exp = await getPublicExpoConfigAsync(projectDir, options);
return {
exp,
projectDir,
Expand All @@ -66,7 +66,7 @@ export class DynamicPrivateProjectConfigContextField extends ContextField<Dynami
}: ContextOptions): Promise<DynamicConfigContextFn> {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
return async (options?: ExpoConfigOptions) => {
const expBefore = getPrivateExpoConfig(projectDir, options);
const expBefore = await getPrivateExpoConfigAsync(projectDir, options);
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive,
env: options?.env,
Expand All @@ -89,7 +89,7 @@ export class DynamicPrivateProjectConfigContextField extends ContextField<Dynami
},
};
}
const exp = getPrivateExpoConfig(projectDir, options);
const exp = await getPrivateExpoConfigAsync(projectDir, options);
return {
exp,
projectDir,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { createGraphqlClient } from './contextUtils/createGraphqlClient';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
import { loadServerSideEnvironmentVariablesAsync } from './contextUtils/loadServerSideEnvironmentVariablesAsync';
import { getPrivateExpoConfig } from '../../project/expoConfig';
import { getPrivateExpoConfigAsync } from '../../project/expoConfig';

export class OptionalPrivateProjectConfigContextField extends ContextField<
| {
Expand Down Expand Up @@ -41,7 +41,7 @@ export class OptionalPrivateProjectConfigContextField extends ContextField<
return undefined;
}

const expBefore = getPrivateExpoConfig(projectDir);
const expBefore = await getPrivateExpoConfigAsync(projectDir);
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive,
});
Expand All @@ -58,7 +58,7 @@ export class OptionalPrivateProjectConfigContextField extends ContextField<
});
serverSideEnvVars = serverSideEnvironmentVariables;
}
const exp = getPrivateExpoConfig(projectDir, { env: serverSideEnvVars });
const exp = await getPrivateExpoConfigAsync(projectDir, { env: serverSideEnvVars });
return {
exp,
projectDir,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { createGraphqlClient } from './contextUtils/createGraphqlClient';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
import { loadServerSideEnvironmentVariablesAsync } from './contextUtils/loadServerSideEnvironmentVariablesAsync';
import { getPrivateExpoConfig } from '../../project/expoConfig';
import { getPrivateExpoConfigAsync } from '../../project/expoConfig';

export class PrivateProjectConfigContextField extends ContextField<{
projectId: string;
Expand All @@ -22,7 +22,7 @@ export class PrivateProjectConfigContextField extends ContextField<{
projectDir: string;
}> {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
const expBefore = getPrivateExpoConfig(projectDir);
const expBefore = await getPrivateExpoConfigAsync(projectDir);
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive,
});
Expand All @@ -39,7 +39,7 @@ export class PrivateProjectConfigContextField extends ContextField<{
});
serverSideEnvVars = serverSideEnvironmentVariables;
}
const exp = getPrivateExpoConfig(projectDir, { env: serverSideEnvVars });
const exp = await getPrivateExpoConfigAsync(projectDir, { env: serverSideEnvVars });

return {
projectId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import ContextField, { ContextOptions } from './ContextField';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
import { getPrivateExpoConfig } from '../../project/expoConfig';
import { getPrivateExpoConfigAsync } from '../../project/expoConfig';

export class ProjectIdContextField extends ContextField<string> {
async getValueAsync({ nonInteractive, sessionManager }: ContextOptions): Promise<string> {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
const expBefore = getPrivateExpoConfig(projectDir);
const expBefore = await getPrivateExpoConfigAsync(projectDir);
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createGraphqlClient } from './contextUtils/createGraphqlClient';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
import { loadServerSideEnvironmentVariablesAsync } from './contextUtils/loadServerSideEnvironmentVariablesAsync';
import { getPublicExpoConfig } from '../../project/expoConfig';
import { getPublicExpoConfigAsync } from '../../project/expoConfig';

type GetServerSideEnvironmentVariablesFn = (
maybeEnv?: Record<string, string>
Expand All @@ -22,7 +22,7 @@ export class ServerSideEnvironmentVariablesContextField extends ContextField<Get
'withServerSideEnvironment parameter is required to evaluate ServerSideEnvironmentVariablesContextField'
);
}
const exp = getPublicExpoConfig(projectDir, { env: maybeEnv });
const exp = await getPublicExpoConfigAsync(projectDir, { env: maybeEnv });
const projectId = await getProjectIdAsync(sessionManager, exp, {
nonInteractive,
env: maybeEnv,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Role } from '../../../../graphql/generated';
import { AppQuery } from '../../../../graphql/queries/AppQuery';
import { learnMore } from '../../../../log';
import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
import { isExpoInstalled } from '../../../../project/projectUtils';
import SessionManager from '../../../../user/SessionManager';
import { findProjectRootAsync } from '../findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from '../getProjectIdAsync';
Expand All @@ -21,6 +22,7 @@ jest.mock('../../../../ora', () => ({
}),
}));
jest.mock('../../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync');
jest.mock('../../../../project/projectUtils');

describe(getProjectIdAsync, () => {
let sessionManager: SessionManager;
Expand Down Expand Up @@ -71,6 +73,7 @@ describe(getProjectIdAsync, () => {
);

jest.mocked(findProjectRootAsync).mockResolvedValue('/app');
jest.mocked(isExpoInstalled).mockReturnValue(true);
});

it('gets the project ID from app config if exists', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import { findProjectRootAsync } from './findProjectDirAndVerifyProjectSetupAsync
import { AppQuery } from '../../../graphql/queries/AppQuery';
import Log, { learnMore } from '../../../log';
import { ora } from '../../../ora';
import { createOrModifyExpoConfigAsync, getPrivateExpoConfig } from '../../../project/expoConfig';
import {
createOrModifyExpoConfigAsync,
getPrivateExpoConfigAsync,
} from '../../../project/expoConfig';
import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
import { toAppPrivacy } from '../../../project/projectUtils';
import SessionManager from '../../../user/SessionManager';
Expand All @@ -25,7 +28,7 @@ export async function saveProjectIdToAppConfigAsync(
options: { env?: Env } = {}
): Promise<void> {
// NOTE(cedric): we disable plugins to avoid writing plugin-generated content to `expo.extra`
const exp = getPrivateExpoConfig(projectDir, { skipPlugins: true, ...options });
const exp = await getPrivateExpoConfigAsync(projectDir, { skipPlugins: true, ...options });
const result = await createOrModifyExpoConfigAsync(
projectDir,
{
Expand Down
3 changes: 3 additions & 0 deletions packages/eas-cli/src/commands/project/__tests__/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { AppMutation } from '../../../graphql/mutations/AppMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { createOrModifyExpoConfigAsync } from '../../../project/expoConfig';
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
import { isExpoInstalled } from '../../../project/projectUtils';
import { confirmAsync, promptAsync } from '../../../prompts';
import ProjectInit from '../init';

Expand All @@ -34,6 +35,7 @@ jest.mock('../../../ora', () => ({
}));
jest.mock('../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync');
jest.mock('../../../commandUtils/context/contextUtils/getProjectIdAsync');
jest.mock('../../../project/projectUtils');

let originalProcessArgv: string[];

Expand Down Expand Up @@ -92,6 +94,7 @@ function mockTestProject(options: {
graphqlClient,
authenticationInfo: { accessToken: null, sessionSecret: '1234' },
});
jest.mocked(isExpoInstalled).mockReturnValue(true);
}

const commandOptions = { root: '/test-project' } as any;
Expand Down
8 changes: 4 additions & 4 deletions packages/eas-cli/src/commands/project/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { AppMutation } from '../../graphql/mutations/AppMutation';
import { AppQuery } from '../../graphql/queries/AppQuery';
import Log, { link } from '../../log';
import { ora } from '../../ora';
import { createOrModifyExpoConfigAsync, getPrivateExpoConfig } from '../../project/expoConfig';
import { createOrModifyExpoConfigAsync, getPrivateExpoConfigAsync } from '../../project/expoConfig';
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
import { toAppPrivacy } from '../../project/projectUtils';
import { Choice, confirmAsync, promptAsync } from '../../prompts';
Expand Down Expand Up @@ -106,7 +106,7 @@ export default class ProjectInit extends EasCommand {
projectDir: string,
{ force, nonInteractive }: InitializeMethodOptions
): Promise<void> {
const exp = getPrivateExpoConfig(projectDir);
const exp = await getPrivateExpoConfigAsync(projectDir);
const appForProjectId = await AppQuery.byIdAsync(graphqlClient, projectId);
const correctOwner = appForProjectId.ownerAccount.name;
const correctSlug = appForProjectId.slug;
Expand Down Expand Up @@ -161,7 +161,7 @@ export default class ProjectInit extends EasCommand {
projectDir: string,
{ force, nonInteractive }: InitializeMethodOptions
): Promise<void> {
const exp = getPrivateExpoConfig(projectDir);
const exp = await getPrivateExpoConfigAsync(projectDir);
const existingProjectId = exp.extra?.eas?.projectId;

if (projectId === existingProjectId) {
Expand Down Expand Up @@ -218,7 +218,7 @@ export default class ProjectInit extends EasCommand {
projectDir: string,
{ force, nonInteractive }: InitializeMethodOptions
): Promise<string> {
const exp = getPrivateExpoConfig(projectDir);
const exp = await getPrivateExpoConfigAsync(projectDir);
const existingProjectId = exp.extra?.eas?.projectId;

if (existingProjectId) {
Expand Down
6 changes: 3 additions & 3 deletions packages/eas-cli/src/commands/project/onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
import { installDependenciesAsync } from '../../onboarding/installDependencies';
import { runCommandAsync } from '../../onboarding/runCommand';
import { RequestedPlatform } from '../../platform';
import { ExpoConfigOptions, getPrivateExpoConfig } from '../../project/expoConfig';
import { ExpoConfigOptions, getPrivateExpoConfigAsync } from '../../project/expoConfig';
import { promptAsync } from '../../prompts';
import { Actor } from '../../user/User';
import { easCliVersion } from '../../utils/easCli';
Expand Down Expand Up @@ -339,7 +339,7 @@ async function getPrivateExpoConfigWithProjectIdAsync({
actor: Actor;
options?: ExpoConfigOptions;
}): Promise<CredentialsContextProjectInfo> {
const expBefore = getPrivateExpoConfig(projectDir, options);
const expBefore = await getPrivateExpoConfigAsync(projectDir, options);
const projectId = await validateOrSetProjectIdAsync({
exp: expBefore,
graphqlClient,
Expand All @@ -349,7 +349,7 @@ async function getPrivateExpoConfigWithProjectIdAsync({
},
cwd: projectDir,
});
const exp = getPrivateExpoConfig(projectDir, options);
const exp = await getPrivateExpoConfigAsync(projectDir, options);
return {
exp,
projectId,
Expand Down
13 changes: 1 addition & 12 deletions packages/eas-cli/src/commands/update/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,18 +140,7 @@ describe(UpdatePublish.name, () => {
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];

// Add configuration to the project that should not be included in the update
const { appJson } = mockTestProject({
expoConfig: {
hooks: {
postPublish: [
{
file: 'custom-hook.js',
config: { some: 'config' },
},
],
},
},
});
const { appJson } = mockTestProject();

const { platforms, runtimeVersion } = mockTestExport({ platforms: ['ios'] });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,18 +145,7 @@ describe(UpdateRollBackToEmbedded.name, () => {
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];

// Add configuration to the project that should not be included in the update
mockTestProject({
expoConfig: {
hooks: {
postPublish: [
{
file: 'custom-hook.js',
config: { some: 'config' },
},
],
},
},
});
mockTestProject();

const platforms = ['ios'];
const runtimeVersion = 'exposdk:47.0.0';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ export function createCtxMock(mockOverride: Record<string, any> = {}): Credentia
},
hasAppleCtx: jest.fn(() => true),
hasProjectContext: true,
exp: testAppJson,
getExpoConfigAsync: async () => testAppJson,
projectDir: '.',
getProjectIdAsync: async () => 'test-project-id',
};
return merge(defaultMock, mockOverride) as any;
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,14 @@ export async function getAppLookupParamsFromContextAsync(
ctx: CredentialsContext,
gradleContext?: GradleBuildContext
): Promise<AppLookupParams> {
ctx.ensureProjectContext();
const projectName = ctx.exp.slug;
const projectId = ctx.projectId;
const exp = await ctx.getExpoConfigAsync();
const projectName = exp.slug;
const projectId = await ctx.getProjectIdAsync();
const account = await getOwnerAccountForProjectIdAsync(ctx.graphqlClient, projectId);

const androidApplicationIdentifier = await getApplicationIdAsync(
ctx.projectDir,
ctx.exp,
exp,
ctx.vcsClient,
gradleContext
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export class CreateKeystore {
throw new Error(`New keystore cannot be created in non-interactive mode.`);
}

const projectId = ctx.projectId;
const projectId = await ctx.getProjectIdAsync();
const keystore = await this.provideOrGenerateAsync(ctx.graphqlClient, ctx.analytics, projectId);
const keystoreFragment = await ctx.android.createKeystoreAsync(
ctx.graphqlClient,
Expand Down
14 changes: 7 additions & 7 deletions packages/eas-cli/src/credentials/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { AuthenticationMode } from './ios/appstore/authenticateTypes';
import { Analytics } from '../analytics/AnalyticsManager';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import Log from '../log';
import { getPrivateExpoConfig } from '../project/expoConfig';
import { getPrivateExpoConfigAsync } from '../project/expoConfig';
import { confirmAsync } from '../prompts';
import { Actor } from '../user/User';
import { Client } from '../vcs/vcs';
Expand Down Expand Up @@ -67,22 +67,22 @@ export class CredentialsContext {
return !!this.projectInfo;
}

get exp(): ExpoConfig {
this.ensureProjectContext();
public async getExpoConfigAsync(): Promise<ExpoConfig> {
await this.ensureProjectContextAsync();
return this.projectInfo!.exp;
}

get projectId(): string {
this.ensureProjectContext();
public async getProjectIdAsync(): Promise<string> {
await this.ensureProjectContextAsync();
return this.projectInfo!.projectId;
}

public ensureProjectContext(): void {
public async ensureProjectContextAsync(): Promise<void> {
if (this.hasProjectContext) {
return;
}
// trigger getConfig error
getPrivateExpoConfig(this.options.projectDir);
await getPrivateExpoConfigAsync(this.options.projectDir);
}

async bestEffortAppStoreAuthenticateAsync(): Promise<void> {
Expand Down
Loading

0 comments on commit ac799bb

Please sign in to comment.