Compare commits

...
Author SHA1 Message Date
expo-tuft[bot]andide d3202a7003 [eas-cli] Apply suggested wording for missing expo package error
Co-authored-by: ide <[email protected]>
2026-07-24 21:30:56 -07:00
expo-tuft[bot]andide cc19ec8ecf [eas-cli] Apply review suggestions to error message and comment
Co-authored-by: ide <[email protected]>
2026-07-24 21:28:21 -07:00
expo-tuft[bot]andide ffc24c92c5 [eas-cli] Clarify comment on bundled @expo/config fallback
Co-authored-by: ide <[email protected]>
2026-07-24 16:14:44 -07:00
expo-tuft[bot]andide a8240eb7ce [eas-cli] Remove test pinning the bundled @expo/config fallback
Co-authored-by: ide <[email protected]>
2026-07-24 16:02:42 -07:00
expo-tuft[bot]andide c8e1799100 Format init.test.ts
Co-authored-by: ide <[email protected]>
2026-07-24 15:05:50 -07:00
expo-tuft[bot]andide 262f8fb01b [eas-cli] Reserve bundled @expo/config fallback for old SDKs without Expo CLI
Co-authored-by: ide <[email protected]>
2026-07-24 15:01:53 -07:00
expo-tuft[bot]andide b3baf0ad7c [eas-cli] Tolerate mocked getPackageJson returning undefined
Co-authored-by: ide <[email protected]>
2026-07-24 14:53:05 -07:00
expo-tuft[bot]andide 6defce7119 [eas-cli] Check for uninstalled dependencies upfront instead of wrapping getConfig
Co-authored-by: ide <[email protected]>
2026-07-24 14:43:03 -07:00
expo-tuft[bot]andide 4880dce0ab [eas-cli] Print actionable error instead of skipping plugins per feedback
Co-authored-by: ide <[email protected]>
2026-07-24 14:36:39 -07:00
expo-tuft[bot]andide 48b0e70af1 Update CHANGELOG.md
Co-authored-by: ide <[email protected]>
2026-07-24 14:30:46 -07:00
expo-tuft[bot]andide 93fc345630 [eas-cli] Read app config without plugins when dependencies aren't installed
Co-authored-by: ide <[email protected]>
2026-07-24 14:29:50 -07:00
5 changed files with 89 additions and 4 deletions

No files matched your search

+2
View File
@@ -13,6 +13,8 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 Bug fixes
- [eas-cli] Print an actionable error message that says to install the project's dependencies when they aren't installed and EAS CLI needs them to read the app config, instead of a confusing module resolution error. Reading the config with the copy of `@expo/config` bundled with EAS CLI is now reserved for old SDK versions whose `expo` package doesn't include Expo CLI; projects without the `expo` package get an error directing them to install it. ([#4080](https://github.com/expo/eas-cli/pull/4080) by [@ide](https://github.com/ide))
### 🧹 Chores
## [21.2.0](https://github.com/expo/eas-cli/releases/tag/v21.2.0) - 2026-07-24
@@ -1,5 +1,6 @@
import { getConfig, getConfigFilePaths, modifyConfigAsync } from '@expo/config';
import { vol } from 'memfs';
import resolveFrom from 'resolve-from';
import { anything, instance, mock, when } from 'ts-mockito';
import { Role } from '../../../../graphql/generated';
@@ -14,6 +15,11 @@ import { getProjectIdAsync } from '../getProjectIdAsync';
jest.mock('@expo/config');
jest.mock('fs');
jest.mock('resolve-from', () => {
const resolveFrom: any = jest.fn();
resolveFrom.silent = jest.fn();
return resolveFrom;
});
jest.mock('../../../../graphql/queries/AppQuery');
jest.mock('../../contextUtils/findProjectDirAndVerifyProjectSetupAsync');
@@ -36,6 +42,10 @@ describe(getProjectIdAsync, () => {
.mocked(getConfigFilePaths)
.mockReturnValue({ staticConfigPath: null, dynamicConfigPath: null });
// Simulate an old SDK project: `expo` is installed but doesn't include Expo CLI, so the
// config is read with the copy of `@expo/config` bundled with EAS CLI.
jest.mocked(resolveFrom.silent).mockReturnValue('/app/node_modules/expo/package.json');
const sessionManagerMock = mock<SessionManager>();
when(sessionManagerMock.ensureLoggedInAsync(anything())).thenResolve({
actor: {
@@ -1,6 +1,7 @@
import { AppJSONConfig, PackageJSONConfig, getConfig } from '@expo/config';
import chalk from 'chalk';
import { vol } from 'memfs';
import resolveFrom from 'resolve-from';
import { instance, mock } from 'ts-mockito';
import { getMockOclifConfig } from '../../../__tests__/commands/utils';
@@ -21,6 +22,11 @@ import ProjectInit from '../init';
jest.mock('fs');
jest.mock('@expo/config');
jest.mock('resolve-from', () => {
const resolveFrom: any = jest.fn();
resolveFrom.silent = jest.fn();
return resolveFrom;
});
jest.mock('../../../project/expoConfig', () => ({
...jest.requireActual('../../../project/expoConfig'),
createOrModifyExpoConfigAsync: jest.fn(),
@@ -99,6 +105,9 @@ function mockTestProject(options: {
// 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);
// Simulate an old SDK project: `expo` is installed but doesn't include Expo CLI, so the config
// is read with the copy of `@expo/config` bundled with EAS CLI.
jest.mocked(resolveFrom.silent).mockReturnValue(projectRoot + '/node_modules/expo/package.json');
}
const commandOptions = getMockOclifConfig({ root: '/test-project' });
@@ -1,12 +1,20 @@
import { getConfigFilePaths, modifyConfigAsync } from '@expo/config';
import { getConfig, getConfigFilePaths, getPackageJson, modifyConfigAsync } from '@expo/config';
import JsonFile from '@expo/json-file';
import { writeFileSync } from 'fs-extra';
import resolveFrom from 'resolve-from';
import { createOrModifyExpoConfigAsync } from '../expoConfig';
import { createOrModifyExpoConfigAsync, getPrivateExpoConfigAsync } from '../expoConfig';
import { isExpoInstalled } from '../projectUtils';
jest.mock('fs-extra');
jest.mock('@expo/config');
jest.mock('@expo/json-file');
jest.mock('resolve-from', () => {
const resolveFrom: any = jest.fn();
resolveFrom.silent = jest.fn();
return resolveFrom;
});
jest.mock('../projectUtils');
beforeEach(() => {
jest.resetAllMocks();
@@ -47,4 +55,34 @@ describe('expoConfig', () => {
expect(modifyConfigAsync).toHaveBeenCalledWith('/app', { owner: 'ccheever' });
});
});
describe('getPrivateExpoConfigAsync when Expo CLI is not resolvable', () => {
beforeEach(() => {
jest.mocked(getConfigFilePaths).mockReturnValue({
staticConfigPath: '/app/app.json',
dynamicConfigPath: null,
});
jest.mocked(isExpoInstalled).mockReturnValue(false);
});
it('throws an actionable error when expo is declared in package.json but not installed', async () => {
jest.mocked(resolveFrom.silent).mockReturnValue(undefined);
jest.mocked(getPackageJson).mockReturnValue({ dependencies: { expo: '~53.0.0' } } as any);
await expect(getPrivateExpoConfigAsync('/app')).rejects.toThrow(
/dependencies to be installed/
);
expect(getConfig).not.toHaveBeenCalled();
});
it('throws an actionable error when expo is not declared in package.json', async () => {
jest.mocked(resolveFrom.silent).mockReturnValue(undefined);
jest.mocked(getPackageJson).mockReturnValue({ dependencies: {} } as any);
await expect(getPrivateExpoConfigAsync('/app')).rejects.toThrow(
/The "expo" package was not found/
);
expect(getConfig).not.toHaveBeenCalled();
});
});
});
+28 -2
View File
@@ -1,10 +1,20 @@
import { ExpoConfig, getConfig, getConfigFilePaths, modifyConfigAsync } from '@expo/config';
import {
ExpoConfig,
getConfig,
getConfigFilePaths,
getPackageJson,
modifyConfigAsync,
} from '@expo/config';
import { Env } from '@expo/eas-build-job';
import { resolvePackageManager } from '@expo/package-manager';
import chalk from 'chalk';
import fs from 'fs-extra';
import Joi from 'joi';
import path from 'path';
import resolveFrom from 'resolve-from';
import { isExpoInstalled } from './projectUtils';
import { link } from '../log';
import { spawnExpoCommand } from '../utils/expoCli';
export type PublicExpoConfig = Omit<
@@ -63,12 +73,28 @@ async function getExpoConfigInternalAsync(
}
);
exp = JSON.parse(stdout);
} else {
} else if (resolveFrom.silent(projectDir, 'expo/package.json')) {
// The `expo` package is installed but Expo CLI is not part of it. This happens with old
// SDK versions, which predate the local CLI. Since we can't run `expo config`, read the app
// config with the copy of `@expo/config` that ships with EAS CLI.
exp = getConfig(projectDir, {
skipSDKVersionRequirement: true,
...(opts.isPublicConfig ? { isPublicConfig: true } : {}),
...(opts.skipPlugins ? { skipPlugins: true } : {}),
}).exp;
} else if (getPackageJson(projectDir)?.dependencies?.expo) {
const installCommand = `${resolvePackageManager(projectDir) ?? 'npm'} install`;
throw new Error(
`EAS CLI needs your project's dependencies to be installed to read your app config. Run ${chalk.bold(
installCommand
)} in your project directory and run this command again.`
);
} else {
throw new Error(
`The "expo" package was not found in your project's dependencies, needed to read your app config. Add "expo" to your dependencies and install it. Refer to the version compatibility table at: ${link(
'https://docs.expo.dev/versions/latest/'
)}`
);
}
const { error } = MinimalAppConfigSchema.validate(exp, {