diff --git a/CHANGELOG.md b/CHANGELOG.md index 79a5322178..d3ea4d5de6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ This is the log of notable changes to EAS CLI and related packages. ### ๐Ÿ› Bug fixes +- [eas-cli] Use development mode when EAS Simulator loads env files. ([#4245](https://github.com/expo/eas-cli/pull/4245) by [@ramonclaudio](https://github.com/ramonclaudio)) + ### ๐Ÿงน Chores ## [22.2.0](https://github.com/expo/eas-cli/releases/tag/v22.2.0) - 2026-08-20 diff --git a/packages/eas-cli/src/commands/simulator/__tests__/exec.test.ts b/packages/eas-cli/src/commands/simulator/__tests__/exec.test.ts index a6959f326e..a10e687191 100644 --- a/packages/eas-cli/src/commands/simulator/__tests__/exec.test.ts +++ b/packages/eas-cli/src/commands/simulator/__tests__/exec.test.ts @@ -5,6 +5,7 @@ import { Config } from '@oclif/core'; import SimulatorExec from '../exec'; jest.mock('@expo/env', () => ({ + LOADED_ENV_NAME: '__EXPO_ENV_LOADED', loadEnvFiles: jest.fn(), loadProjectEnv: jest.fn(), })); @@ -20,14 +21,20 @@ function getMockOclifConfig(): Config { } describe(SimulatorExec, () => { + const originalEnv = process.env; const mockConfig = getMockOclifConfig(); const projectDir = '/test/project'; beforeEach(() => { jest.clearAllMocks(); + process.env = { ...originalEnv }; jest.mocked(spawnAsync).mockResolvedValue({} as never); }); + afterEach(() => { + process.env = originalEnv; + }); + function createCommand(argv: string[]): { command: SimulatorExec; getContextAsync: jest.SpyInstance; @@ -48,7 +55,11 @@ describe(SimulatorExec, () => { expect(getContextAsync).toHaveBeenCalledWith(SimulatorExec, { nonInteractive: true, }); - expect(loadProjectEnv).toHaveBeenCalledWith(projectDir, { silent: true }); + expect(loadProjectEnv).toHaveBeenCalledWith(projectDir, { + force: true, + mode: 'development', + silent: true, + }); expect(loadEnvFiles).toHaveBeenCalledWith([`${projectDir}/.env.eas-simulator`], { force: true, }); @@ -118,17 +129,4 @@ describe(SimulatorExec, () => { expect(loadEnvFiles).not.toHaveBeenCalled(); expect(spawnAsync).not.toHaveBeenCalled(); }); - - it('loads simulator-specific env after regular env files', async () => { - const { command } = createCommand(['agent-device', 'touch', '@e2']); - await command.runAsync(); - - expect(loadProjectEnv).toHaveBeenCalledWith(projectDir, { silent: true }); - expect(loadEnvFiles).toHaveBeenCalledWith([`${projectDir}/.env.eas-simulator`], { - force: true, - }); - expect(jest.mocked(loadProjectEnv).mock.invocationCallOrder[0]).toBeLessThan( - jest.mocked(loadEnvFiles).mock.invocationCallOrder[0] - ); - }); }); diff --git a/packages/eas-cli/src/simulator/__tests__/env.integration.test.ts b/packages/eas-cli/src/simulator/__tests__/env.integration.test.ts new file mode 100644 index 0000000000..deb36cdaa3 --- /dev/null +++ b/packages/eas-cli/src/simulator/__tests__/env.integration.test.ts @@ -0,0 +1,63 @@ +import { LOADED_ENV_NAME } from '@expo/env'; +import * as fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +import { loadSimulatorEnvAsync } from '../env'; + +describe(loadSimulatorEnvAsync, () => { + const originalEnv = process.env; + let projectDir: string; + + beforeEach(async () => { + projectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-simulator-env-')); + process.env = { + ...originalEnv, + PARENT_DOTENV_VALUE: 'from-parent', + SHARED_WITH_SHELL: 'from-shell', + NODE_ENV: 'production', + [LOADED_ENV_NAME]: '["PARENT_DOTENV_VALUE"]', + __EXPO_CONFIG_MODE: 'production', + }; + delete process.env.EXPO_NO_DOTENV; + }); + + afterEach(async () => { + process.env = originalEnv; + await fs.rm(projectDir, { force: true, recursive: true }); + }); + + it('keeps shell values and gives simulator values priority over project dotenv values', async () => { + await fs.writeFile( + path.join(projectDir, '.env.development'), + [ + 'PROJECT_VALUE=from-project', + 'SHARED_WITH_SIMULATOR=from-project', + 'SHARED_WITH_SHELL=from-project', + '__EXPO_CONFIG_MODE=from-project', + ].join('\n') + ); + await fs.writeFile( + path.join(projectDir, '.env.eas-simulator'), + [ + 'SIMULATOR_VALUE=from-simulator', + 'SHARED_WITH_SIMULATOR=from-simulator', + 'SHARED_WITH_SHELL=from-simulator', + '__EXPO_CONFIG_MODE=from-simulator', + ].join('\n') + ); + + await loadSimulatorEnvAsync(projectDir); + + expect(process.env).toMatchObject({ + NODE_ENV: 'development', + PROJECT_VALUE: 'from-project', + SHARED_WITH_SHELL: 'from-shell', + SHARED_WITH_SIMULATOR: 'from-simulator', + SIMULATOR_VALUE: 'from-simulator', + }); + expect(process.env.PARENT_DOTENV_VALUE).toBeUndefined(); + expect(process.env[LOADED_ENV_NAME]).toBeUndefined(); + expect(process.env.__EXPO_CONFIG_MODE).toBeUndefined(); + }); +}); diff --git a/packages/eas-cli/src/simulator/__tests__/env.test.ts b/packages/eas-cli/src/simulator/__tests__/env.test.ts index cce1ab2868..e709923f4a 100644 --- a/packages/eas-cli/src/simulator/__tests__/env.test.ts +++ b/packages/eas-cli/src/simulator/__tests__/env.test.ts @@ -1,3 +1,4 @@ +import { loadEnvFiles, loadProjectEnv } from '@expo/env'; import * as fs from 'fs-extra'; import { parse as parseDotenv } from 'dotenv'; @@ -5,12 +6,80 @@ import { EAS_SIMULATOR_SESSION_ID, SIMULATOR_DOTENV_FILE_HEADER, getSimulatorEnvFilePath, + loadSimulatorEnvAsync, resetSimulatorEnvAsync, writeSimulatorEnvAsync, } from '../env'; +jest.mock('@expo/env', () => ({ + LOADED_ENV_NAME: '__EXPO_ENV_LOADED', + loadEnvFiles: jest.fn(), + loadProjectEnv: jest.fn(), +})); jest.mock('fs-extra'); +describe(loadSimulatorEnvAsync, () => { + const projectDir = '/test/project'; + const originalEnv = process.env; + + beforeEach(() => { + jest.clearAllMocks(); + process.env = { + ...originalEnv, + DOTENV_VALUE: 'from-parent', + KEEP_VALUE: 'from-shell', + NODE_ENV: 'staging', + __EXPO_ENV_LOADED: '["DOTENV_VALUE"]', + __EXPO_CONFIG_MODE: 'production', + }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it('loads simulator env before project env files in development mode', async () => { + jest.mocked(loadEnvFiles).mockImplementation(() => { + expect(process.env.DOTENV_VALUE).toBeUndefined(); + expect(process.env.KEEP_VALUE).toBe('from-shell'); + expect(process.env.NODE_ENV).toBe('development'); + expect(process.env.__EXPO_ENV_LOADED).toBeUndefined(); + expect(process.env.__EXPO_CONFIG_MODE).toBeUndefined(); + process.env.SIMULATOR_VALUE = 'from-simulator'; + process.env.__EXPO_ENV_LOADED = '["SIMULATOR_VALUE"]'; + process.env.__EXPO_CONFIG_MODE = 'from-simulator-env'; + return {} as never; + }); + jest.mocked(loadProjectEnv).mockImplementation(() => { + expect(process.env.SIMULATOR_VALUE).toBe('from-simulator'); + expect(process.env.__EXPO_ENV_LOADED).toBeUndefined(); + expect(process.env.__EXPO_CONFIG_MODE).toBeUndefined(); + process.env.PROJECT_VALUE = 'from-project'; + process.env.__EXPO_ENV_LOADED = '["PROJECT_VALUE"]'; + process.env.__EXPO_CONFIG_MODE = 'from-project-env'; + return {} as never; + }); + + await loadSimulatorEnvAsync(projectDir); + + expect(loadEnvFiles).toHaveBeenCalledWith([`${projectDir}/.env.eas-simulator`], { + force: true, + }); + expect(loadProjectEnv).toHaveBeenCalledWith(projectDir, { + force: true, + mode: 'development', + silent: true, + }); + expect(jest.mocked(loadEnvFiles).mock.invocationCallOrder[0]).toBeLessThan( + jest.mocked(loadProjectEnv).mock.invocationCallOrder[0] + ); + expect(process.env.SIMULATOR_VALUE).toBe('from-simulator'); + expect(process.env.PROJECT_VALUE).toBe('from-project'); + expect(process.env.__EXPO_ENV_LOADED).toBeUndefined(); + expect(process.env.__EXPO_CONFIG_MODE).toBeUndefined(); + }); +}); + describe(resetSimulatorEnvAsync, () => { const projectDir = '/test/project'; const simulatorDotenvPath = getSimulatorEnvFilePath(projectDir); diff --git a/packages/eas-cli/src/simulator/env.ts b/packages/eas-cli/src/simulator/env.ts index 9c69740df6..2dbfe9c8a0 100644 --- a/packages/eas-cli/src/simulator/env.ts +++ b/packages/eas-cli/src/simulator/env.ts @@ -1,9 +1,10 @@ -import { loadEnvFiles, loadProjectEnv } from '@expo/env'; +import { LOADED_ENV_NAME, loadEnvFiles, loadProjectEnv } from '@expo/env'; import { parse as parseDotenv } from 'dotenv'; import * as fs from 'fs-extra'; import path from 'path'; import Log from '../log'; +import { getEnvWithoutInheritedDotenvValues } from '../utils/originalEnv'; export const SIMULATOR_DOTENV_FILE_NAME = '.env.eas-simulator'; export const EAS_SIMULATOR_SESSION_ID = 'EAS_SIMULATOR_SESSION_ID'; @@ -16,9 +17,17 @@ export function getSimulatorEnvFilePath(projectDir: string): string { export async function loadSimulatorEnvAsync(projectDir: string): Promise { const simulatorDotenvFilePath = getSimulatorEnvFilePath(projectDir); + const mode = 'development'; - loadProjectEnv(projectDir, { silent: true }); + process.env = getEnvWithoutInheritedDotenvValues(process.env); + process.env.NODE_ENV = mode; + delete process.env.__EXPO_CONFIG_MODE; loadEnvFiles([simulatorDotenvFilePath], { force: true }); + delete process.env[LOADED_ENV_NAME]; + delete process.env.__EXPO_CONFIG_MODE; + loadProjectEnv(projectDir, { force: true, mode, silent: true }); + delete process.env[LOADED_ENV_NAME]; + delete process.env.__EXPO_CONFIG_MODE; } export async function writeSimulatorEnvAsync(