Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ This is the log of notable changes to EAS CLI and related packages.

### 🎉 New features

- Warn in `eas build` when creating a production build from an app that uses Expo Go for development ([#3073](https://github.com/expo/eas-cli/pull/3073) by [@vonovak](https://github.com/vonovak))

### 🐛 Bug fixes

### 🧹 Chores
Expand Down
17 changes: 8 additions & 9 deletions packages/eas-cli/src/build/createContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { createAndroidContextAsync } from './android/build';
import { BuildContext, CommonContext } from './context';
import { createIosContextAsync } from './ios/build';
import { LocalBuildMode, LocalBuildOptions } from './local';
import { resolveBuildResourceClassAsync } from './utils/resourceClass';
import { resolveBuildResourceClass } from './utils/resourceClass';
import { Analytics, AnalyticsEventProperties, BuildEvent } from '../analytics/AnalyticsManager';
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
Expand Down Expand Up @@ -73,8 +73,12 @@ export async function createBuildContextAsync<T extends Platform>({
env,
});
const projectName = exp.slug;
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
const workflow = await resolveWorkflowAsync(projectDir, platform, vcsClient);

const [account, workflow] = await Promise.all([
getOwnerAccountForProjectIdAsync(graphqlClient, projectId),
resolveWorkflowAsync(projectDir, platform, vcsClient),
]);

const accountId = account.id;
const runFromCI = getenv.boolish('CI', false);
const developmentClient =
Expand Down Expand Up @@ -118,12 +122,7 @@ export async function createBuildContextAsync<T extends Platform>({
};
analytics.logEvent(BuildEvent.BUILD_COMMAND, analyticsEventProperties);

const resourceClass = await resolveBuildResourceClassAsync(
buildProfile,
platform,
resourceClassFlag
);

const resourceClass = resolveBuildResourceClass(buildProfile, platform, resourceClassFlag);
const commonContext: CommonContext<T> = {
accountName: account.name,
buildProfile,
Expand Down
3 changes: 3 additions & 0 deletions packages/eas-cli/src/build/runBuildAndSubmit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
CustomBuildConfigMetadata,
validateCustomBuildConfigAsync,
} from '../project/customBuildConfig';
import { discourageExpoGoForProdAsync } from '../project/discourageExpoGoForProd';
import { checkExpoSdkIsSupportedAsync } from '../project/expoSdk';
import { validateMetroConfigForManagedWorkflowAsync } from '../project/metroConfig';
import {
Expand Down Expand Up @@ -146,6 +147,8 @@ export async function runBuildAndSubmitAsync({
projectDir,
});

await discourageExpoGoForProdAsync(buildProfiles, projectDir, vcsClient);

for (const buildProfile of buildProfiles) {
if (buildProfile.profile.image && ['default', 'stable'].includes(buildProfile.profile.image)) {
Log.warn(
Expand Down
4 changes: 2 additions & 2 deletions packages/eas-cli/src/build/utils/devClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
nonInteractive?: boolean;
buildProfiles?: ProfileData<'build'>[];
}): Promise<void> {
if (await isExpoDevClientInstalledAsync(projectDir)) {
if (isExpoDevClientInstalled(projectDir)) {
return;
}

Expand Down Expand Up @@ -103,7 +103,7 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
}
}

async function isExpoDevClientInstalledAsync(projectDir: string): Promise<boolean> {
export function isExpoDevClientInstalled(projectDir: string): boolean {
try {
resolveFrom(projectDir, 'expo-dev-client/package.json');
return true;
Expand Down
4 changes: 2 additions & 2 deletions packages/eas-cli/src/build/utils/resourceClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ const androidResourceClassToBuildResourceClassMapping: Record<
[ResourceClass.MEDIUM]: BuildResourceClass.AndroidMedium,
};

export async function resolveBuildResourceClassAsync<T extends Platform>(
export function resolveBuildResourceClass<T extends Platform>(
profile: BuildProfile<T>,
platform: Platform,
resourceClassFlag?: ResourceClass
): Promise<BuildResourceClass> {
): BuildResourceClass {
const profileResourceClass = profile.resourceClass;

if (profileResourceClass && resourceClassFlag && resourceClassFlag !== profileResourceClass) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import getenv from 'getenv';
import resolveFrom from 'resolve-from';

import type { ProfileData } from '../../utils/profiles';
import { resolveVcsClient } from '../../vcs';
import { detectExpoGoProdBuildAsync } from '../discourageExpoGoForProdAsync';

jest.mock('getenv');
jest.mock('resolve-from');
jest.mock('../workflow', () => ({
resolveWorkflowPerPlatformAsync: jest.fn(),
}));

const mockResolveWorkflowPerPlatformAsync = jest.mocked(
require('../workflow').resolveWorkflowPerPlatformAsync
);

const projectDir = '/app';
const vcsClient = resolveVcsClient();

const createMockBuildProfile = (profileName: string): ProfileData<'build'> => ({
profileName,
platform: Platform.ANDROID,
profile: {} as any,
});

describe(detectExpoGoProdBuildAsync, () => {
beforeEach(() => {
jest.clearAllMocks();
jest.mocked(getenv.boolish).mockReturnValue(false);
jest.mocked(resolveFrom).mockImplementation(() => {
// expo-dev-client is not installed
throw new Error('Module not found');
});
});

describe('should return false', () => {
it.each([
['non-production profiles', [createMockBuildProfile('development')]],
['undefined buildProfiles', undefined],
['empty buildProfiles', []],
])('should return false for %s', async (_, buildProfiles) => {
const result = await detectExpoGoProdBuildAsync(buildProfiles, projectDir, vcsClient);

expect(result).toBe(false);
expect(mockResolveWorkflowPerPlatformAsync).not.toHaveBeenCalled();
});

it('when expo-dev-client is installed - that signals a development build', async () => {
jest.mocked(resolveFrom).mockReturnValue('/path/to/expo-dev-client/package.json');
const buildProfiles = [createMockBuildProfile('production')];

const result = await detectExpoGoProdBuildAsync(buildProfiles, projectDir, vcsClient);

expect(result).toBe(false);
expect(mockResolveWorkflowPerPlatformAsync).not.toHaveBeenCalled();
});

it('when either platform is "generic" - likely a bare RN project', async () => {
mockResolveWorkflowPerPlatformAsync.mockResolvedValue({
android: Workflow.GENERIC,
ios: Workflow.GENERIC,
});
const buildProfiles = [createMockBuildProfile('production')];

const result = await detectExpoGoProdBuildAsync(buildProfiles, projectDir, vcsClient);

expect(result).toBe(false);
});
});

describe('should return true', () => {
it('when production profile is used, there are no native directories (or are gitignored) AND expo-dev-client is not installed', async () => {
mockResolveWorkflowPerPlatformAsync.mockResolvedValue({
android: Workflow.MANAGED,
ios: Workflow.MANAGED,
});
const buildProfiles = [createMockBuildProfile('production')];

const result = await detectExpoGoProdBuildAsync(buildProfiles, projectDir, vcsClient);

expect(result).toBe(true);
});
});
});
73 changes: 73 additions & 0 deletions packages/eas-cli/src/project/discourageExpoGoForProd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Workflow } from '@expo/eas-build-job';
import chalk from 'chalk';
import getenv from 'getenv';

import { resolveWorkflowPerPlatformAsync } from './workflow';
import { isExpoDevClientInstalled } from '../build/utils/devClient';
import Log, { learnMore } from '../log';
import type { ProfileData } from '../utils/profiles';
import type { Client } from '../vcs/vcs';

const suppressionEnvVarName = 'EAS_BUILD_NO_EXPO_GO_WARNING';

export async function discourageExpoGoForProdAsync(
buildProfiles: ProfileData<'build'>[] | undefined,
projectDir: string,
vcsClient: Client
): Promise<void> {
try {
const isExpoGoProdBuild = await detectExpoGoProdBuildAsync(
buildProfiles,
projectDir,
vcsClient
);
if (!isExpoGoProdBuild) {
return;
}
Log.newLine();
Log.warn(
`⚠️ It appears you're trying to build an app based on Expo Go for production. Expo Go is not a suitable environment for production apps.`
);
Log.warn(
learnMore('https://docs.expo.dev/develop/development-builds/expo-go-to-dev-build/', {
learnMoreMessage: 'Learn more about converting from Expo Go to a development build',
dim: false,
})
);
Comment on lines +28 to +36
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'll think about how to improve the wording on this and get back to you, let's hold off on merging for now

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Detected that your app uses Expo Go for development, this is not recommended when building production apps. (fyi link here)


fyi page:

Expo Go is meant for learning and prototyping. Learn more.

Behavior of production builds may differ significantly from the Expo Go app, because it is a precompiled sandbox app and your customizations that will apply to production apps and development will likely not be testable in Expo Go.

For example, if you add a library with native code that is not in the Expo SDK, that will not be available in Expo Go. If that library does not actually compile due to an error or incompatibility, you will only discover this when you run a production build. Additionally, most fields in your app.json do not have any impact on your app when it runs in Expo Go, but they will apply when you run a production or development build. Examples of such properties are: scheme, splash (only the app icon is used in Expo Go), any plugins, edgeToEdgeEnabled, predictiveBackGestureEnabled, and so on.

Development builds provide a reliable and flexible development environment, and behave more predictably and similar to production builds. This makes it easier to catch potential issues during development.

Learn more about converting from Expo Go to a development build.

Log.warn(
chalk.dim(`To suppress this warning, set ${chalk.bold(`${suppressionEnvVarName}=true`)}.`)
);
Log.newLine();
} catch (err) {
Log.warn('Error detecting whether Expo Go is used:', err);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only show this is debug, ideally log this to sentry also so we can get some info on when/how this happens

}
}

export async function detectExpoGoProdBuildAsync(
buildProfiles: ProfileData<'build'>[] | undefined,
projectDir: string,
vcsClient: Client
): Promise<boolean> {
const shouldSuppressWarning = getenv.boolish(suppressionEnvVarName, false);

const isProductionBuild = buildProfiles?.map(it => it.profileName).includes('production');
if (shouldSuppressWarning || !isProductionBuild) {
return false;
}

const hasExpoDevClient = isExpoDevClientInstalled(projectDir);
if (hasExpoDevClient) {
return false;
}

return await checkIfManagedWorkflowAsync(projectDir, vcsClient);
}

async function checkIfManagedWorkflowAsync(
projectDir: string,
vcsClient: Client
): Promise<boolean> {
const workflows = await resolveWorkflowPerPlatformAsync(projectDir, vcsClient);

return workflows.android === Workflow.MANAGED && workflows.ios === Workflow.MANAGED;
}
Loading