Skip to content

Commit 9107eb9

Browse files
committed
[eas-build] discourage Expo Go for production
1 parent ce8e098 commit 9107eb9

File tree

6 files changed

+171
-11
lines changed

6 files changed

+171
-11
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ This is the log of notable changes to EAS CLI and related packages.
88

99
### 🎉 New features
1010

11+
- 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))
12+
1113
### 🐛 Bug fixes
1214

1315
### 🧹 Chores

packages/eas-cli/src/build/createContext.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { createAndroidContextAsync } from './android/build';
1111
import { BuildContext, CommonContext } from './context';
1212
import { createIosContextAsync } from './ios/build';
1313
import { LocalBuildMode, LocalBuildOptions } from './local';
14-
import { resolveBuildResourceClassAsync } from './utils/resourceClass';
14+
import { resolveBuildResourceClass } from './utils/resourceClass';
1515
import { Analytics, AnalyticsEventProperties, BuildEvent } from '../analytics/AnalyticsManager';
1616
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
1717
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
@@ -73,8 +73,12 @@ export async function createBuildContextAsync<T extends Platform>({
7373
env,
7474
});
7575
const projectName = exp.slug;
76-
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
77-
const workflow = await resolveWorkflowAsync(projectDir, platform, vcsClient);
76+
77+
const [account, workflow] = await Promise.all([
78+
getOwnerAccountForProjectIdAsync(graphqlClient, projectId),
79+
resolveWorkflowAsync(projectDir, platform, vcsClient),
80+
]);
81+
7882
const accountId = account.id;
7983
const runFromCI = getenv.boolish('CI', false);
8084
const developmentClient =
@@ -118,12 +122,7 @@ export async function createBuildContextAsync<T extends Platform>({
118122
};
119123
analytics.logEvent(BuildEvent.BUILD_COMMAND, analyticsEventProperties);
120124

121-
const resourceClass = await resolveBuildResourceClassAsync(
122-
buildProfile,
123-
platform,
124-
resourceClassFlag
125-
);
126-
125+
const resourceClass = resolveBuildResourceClass(buildProfile, platform, resourceClassFlag);
127126
const commonContext: CommonContext<T> = {
128127
accountName: account.name,
129128
buildProfile,

packages/eas-cli/src/build/runBuildAndSubmit.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import {
4949
CustomBuildConfigMetadata,
5050
validateCustomBuildConfigAsync,
5151
} from '../project/customBuildConfig';
52+
import { discourageExpoGoForProd } from '../project/discourageExpoGoForProd';
5253
import { checkExpoSdkIsSupportedAsync } from '../project/expoSdk';
5354
import { validateMetroConfigForManagedWorkflowAsync } from '../project/metroConfig';
5455
import {
@@ -146,6 +147,8 @@ export async function runBuildAndSubmitAsync({
146147
projectDir,
147148
});
148149

150+
discourageExpoGoForProd(buildProfiles, projectDir, vcsClient);
151+
149152
for (const buildProfile of buildProfiles) {
150153
if (buildProfile.profile.image && ['default', 'stable'].includes(buildProfile.profile.image)) {
151154
Log.warn(

packages/eas-cli/src/build/utils/resourceClass.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ const androidResourceClassToBuildResourceClassMapping: Record<
2828
[ResourceClass.MEDIUM]: BuildResourceClass.AndroidMedium,
2929
};
3030

31-
export async function resolveBuildResourceClassAsync<T extends Platform>(
31+
export function resolveBuildResourceClass<T extends Platform>(
3232
profile: BuildProfile<T>,
3333
platform: Platform,
3434
resourceClassFlag?: ResourceClass
35-
): Promise<BuildResourceClass> {
35+
): BuildResourceClass {
3636
const profileResourceClass = profile.resourceClass;
3737

3838
if (profileResourceClass && resourceClassFlag && resourceClassFlag !== profileResourceClass) {
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { Platform, Workflow } from '@expo/eas-build-job';
2+
import getenv from 'getenv';
3+
import resolveFrom from 'resolve-from';
4+
5+
import type { ProfileData } from '../../utils/profiles';
6+
import { resolveVcsClient } from '../../vcs';
7+
import { detectExpoGoProdBuildAsync } from '../discourageExpoGoForProd';
8+
9+
jest.mock('getenv');
10+
jest.mock('resolve-from');
11+
jest.mock('../workflow', () => ({
12+
resolveWorkflowPerPlatformAsync: jest.fn(),
13+
}));
14+
15+
const mockResolveWorkflowPerPlatformAsync = jest.mocked(
16+
require('../workflow').resolveWorkflowPerPlatformAsync
17+
);
18+
19+
const projectDir = '/app';
20+
const vcsClient = resolveVcsClient();
21+
22+
const createMockBuildProfile = (profileName: string): ProfileData<'build'> => ({
23+
profileName,
24+
platform: Platform.ANDROID,
25+
profile: {} as any,
26+
});
27+
28+
describe(detectExpoGoProdBuildAsync, () => {
29+
beforeEach(() => {
30+
jest.clearAllMocks();
31+
jest.mocked(getenv.boolish).mockReturnValue(false);
32+
jest.mocked(resolveFrom.silent).mockReturnValue(undefined); // expo-dev-client is not installed
33+
});
34+
35+
describe('should return false', () => {
36+
it.each([
37+
['non-production profiles', [createMockBuildProfile('development')]],
38+
['undefined buildProfiles', undefined],
39+
['empty buildProfiles', []],
40+
])('should return false for %s', async (_, buildProfiles) => {
41+
const result = await detectExpoGoProdBuildAsync(buildProfiles, projectDir, vcsClient);
42+
43+
expect(result).toBe(false);
44+
expect(mockResolveWorkflowPerPlatformAsync).not.toHaveBeenCalled();
45+
});
46+
47+
it('when expo-dev-client is installed - that signals a development build', async () => {
48+
jest.mocked(resolveFrom.silent).mockReturnValue('/path/to/expo-dev-client/package.json');
49+
const buildProfiles = [createMockBuildProfile('production')];
50+
51+
const result = await detectExpoGoProdBuildAsync(buildProfiles, projectDir, vcsClient);
52+
53+
expect(result).toBe(false);
54+
expect(mockResolveWorkflowPerPlatformAsync).not.toHaveBeenCalled();
55+
});
56+
57+
it('when either platform is "generic" - likely a bare RN project', async () => {
58+
mockResolveWorkflowPerPlatformAsync.mockResolvedValue({
59+
android: Workflow.GENERIC,
60+
ios: Workflow.GENERIC,
61+
});
62+
const buildProfiles = [createMockBuildProfile('production')];
63+
64+
const result = await detectExpoGoProdBuildAsync(buildProfiles, projectDir, vcsClient);
65+
66+
expect(result).toBe(false);
67+
});
68+
});
69+
70+
describe('should return true', () => {
71+
it('when production profile is used, there are no native directories (or are gitignored) AND expo-dev-client is not installed', async () => {
72+
mockResolveWorkflowPerPlatformAsync.mockResolvedValue({
73+
android: Workflow.MANAGED,
74+
ios: Workflow.MANAGED,
75+
});
76+
const buildProfiles = [createMockBuildProfile('production')];
77+
78+
const result = await detectExpoGoProdBuildAsync(buildProfiles, projectDir, vcsClient);
79+
80+
expect(result).toBe(true);
81+
});
82+
});
83+
});
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Workflow } from '@expo/eas-build-job';
2+
import chalk from 'chalk';
3+
import getenv from 'getenv';
4+
import resolveFrom from 'resolve-from';
5+
6+
import { resolveWorkflowPerPlatformAsync } from './workflow';
7+
import Log, { learnMore } from '../log';
8+
import type { ProfileData } from '../utils/profiles';
9+
import type { Client } from '../vcs/vcs';
10+
11+
const suppressionEnvVarName = 'EAS_BUILD_NO_EXPO_GO_WARNING';
12+
13+
export const discourageExpoGoForProd = (
14+
buildProfiles: ProfileData<'build'>[] | undefined,
15+
projectDir: string,
16+
vcsClient: Client
17+
): void => {
18+
detectExpoGoProdBuildAsync(buildProfiles, projectDir, vcsClient)
19+
.then(usesExpoGo => {
20+
if (usesExpoGo) {
21+
Log.newLine();
22+
Log.warn(
23+
`⚠️ 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.`
24+
);
25+
Log.warn(
26+
learnMore('https://docs.expo.dev/develop/development-builds/expo-go-to-dev-build/', {
27+
learnMoreMessage: 'Learn more about converting from Expo Go to a development build',
28+
dim: false,
29+
})
30+
);
31+
Log.warn(
32+
chalk.dim(`To suppress this warning, set ${chalk.bold(`${suppressionEnvVarName}=true`)}.`)
33+
);
34+
Log.newLine();
35+
}
36+
})
37+
.catch(err => {
38+
Log.warn('Error detecting whether Expo Go is used:', err);
39+
});
40+
};
41+
42+
export async function detectExpoGoProdBuildAsync(
43+
buildProfiles: ProfileData<'build'>[] | undefined,
44+
projectDir: string,
45+
vcsClient: Client
46+
): Promise<boolean> {
47+
const shouldSuppressWarning = getenv.boolish(suppressionEnvVarName, false);
48+
49+
const isProductionBuild = buildProfiles?.map(it => it.profileName).includes('production');
50+
if (shouldSuppressWarning || !isProductionBuild) {
51+
return false;
52+
}
53+
54+
const hasExpoDevClient = checkIfExpoDevClientInstalled(projectDir);
55+
if (hasExpoDevClient) {
56+
return false;
57+
}
58+
59+
return await checkIfManagedWorkflowAsync(projectDir, vcsClient);
60+
}
61+
62+
async function checkIfManagedWorkflowAsync(
63+
projectDir: string,
64+
vcsClient: Client
65+
): Promise<boolean> {
66+
const workflows = await resolveWorkflowPerPlatformAsync(projectDir, vcsClient);
67+
68+
return workflows.android === Workflow.MANAGED && workflows.ios === Workflow.MANAGED;
69+
}
70+
71+
function checkIfExpoDevClientInstalled(projectDir: string): boolean {
72+
return resolveFrom.silent(projectDir, 'expo-dev-client/package.json') !== undefined;
73+
}

0 commit comments

Comments
 (0)