Skip to content

Commit c801227

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

File tree

4 files changed

+162
-6
lines changed

4 files changed

+162
-6
lines changed

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ export async function createBuildContextAsync<T extends Platform>({
7575
const projectName = exp.slug;
7676
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
7777
const workflow = await resolveWorkflowAsync(projectDir, platform, vcsClient);
78+
const resourceClass = await resolveBuildResourceClassAsync(
79+
buildProfile,
80+
platform,
81+
resourceClassFlag
82+
);
7883
const accountId = account.id;
7984
const runFromCI = getenv.boolish('CI', false);
8085
const developmentClient =
@@ -118,12 +123,6 @@ export async function createBuildContextAsync<T extends Platform>({
118123
};
119124
analytics.logEvent(BuildEvent.BUILD_COMMAND, analyticsEventProperties);
120125

121-
const resourceClass = await resolveBuildResourceClassAsync(
122-
buildProfile,
123-
platform,
124-
resourceClassFlag
125-
);
126-
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
@@ -79,6 +79,7 @@ import { truthy } from '../utils/expodash/filter';
7979
import { printJsonOnlyOutput } from '../utils/json';
8080
import { ProfileData, getProfilesAsync } from '../utils/profiles';
8181
import { Client } from '../vcs/vcs';
82+
import { discourageExpoGoForProd } from '../project/discourageExpoGoForProd';
8283

8384
let metroConfigValidated = false;
8485
let sdkVersionChecked = false;
@@ -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(
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 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: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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 envVarName = 'EAS_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(chalk.dim(`To suppress this warning, set ${chalk.bold(`${envVarName}=true`)}.`));
32+
Log.newLine();
33+
}
34+
})
35+
.catch(err => {
36+
Log.warn('Error detecting whether Expo Go is used:', err);
37+
});
38+
};
39+
40+
export async function detectExpoGoProdBuildAsync(
41+
buildProfiles: ProfileData<'build'>[] | undefined,
42+
projectDir: string,
43+
vcsClient: Client
44+
): Promise<boolean> {
45+
const shouldSuppressWarning = getenv.boolish(envVarName, false);
46+
47+
const isProductionBuild = buildProfiles?.map(it => it.profileName).includes('production');
48+
if (shouldSuppressWarning || !isProductionBuild) {
49+
return false;
50+
}
51+
52+
const hasExpoDevClient = checkIfExpoDevClientInstalled(projectDir);
53+
if (hasExpoDevClient) {
54+
return false;
55+
}
56+
57+
return await checkIfManagedWorkflowAsync(projectDir, vcsClient);
58+
}
59+
60+
async function checkIfManagedWorkflowAsync(
61+
projectDir: string,
62+
vcsClient: Client
63+
): Promise<boolean> {
64+
const workflows = await resolveWorkflowPerPlatformAsync(projectDir, vcsClient);
65+
66+
return workflows.android === Workflow.MANAGED && workflows.ios === Workflow.MANAGED;
67+
}
68+
69+
function checkIfExpoDevClientInstalled(projectDir: string): boolean {
70+
return resolveFrom.silent(projectDir, 'expo-dev-client/package.json') !== undefined;
71+
}

0 commit comments

Comments
 (0)