Compare commits

...
13 changed files with 116 additions and 34 deletions

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
---
'eas-cli': patch
'@expo/build-tools': patch
'@expo/turtle-spawn': patch
---
Prevent stdin waits in Expo CLI config, prebuild, and export commands by closing stdin and preserving explicit stdio options when a logger is provided.
+1 -2
View File
@@ -1,6 +1,5 @@
import { BuildJob } from '@expo/eas-build-job';
import { bunyan } from '@expo/logger';
import { SpawnOptions } from '@expo/turtle-spawn';
import {
installDependenciesWithNpmCacheFallbackAsync,
@@ -17,7 +16,7 @@ export async function prebuildAsync<TJob extends BuildJob>(
ctx: BuildContext<TJob>,
{ logger, workingDir, options }: { logger: bunyan; workingDir: string; options?: PrebuildOptions }
): Promise<void> {
const spawnOptions: SpawnOptions = {
const spawnOptions = {
cwd: workingDir,
logger,
env: {
@@ -44,7 +44,6 @@ describe(createEasExportBuildFunction, () => {
cwd: buildStep.ctx.workingDirectory,
env: expect.objectContaining({ HOME: '/tmp/home' }),
logger: buildStep.ctx.logger,
stdio: 'pipe',
},
});
expect(buildStep.outputById.export_dir.value).toBe('dist');
@@ -89,7 +89,6 @@ export function createEasExportBuildFunction(): BuildFunction {
cwd: stepsCtx.workingDirectory,
env,
logger: stepsCtx.logger,
stdio: 'pipe',
},
});
outputs.export_dir.set(outputDir);
@@ -1,9 +1,9 @@
import { Platform } from '@expo/config';
import { BuildFunction, BuildStepInput, BuildStepInputValueTypeName } from '@expo/steps';
import spawn from '@expo/turtle-spawn';
import { installNodeModules } from './installNodeModules';
import { PackageManager, resolvePackageManager } from '../../utils/packageManager';
import { resolvePackageManager } from '../../utils/packageManager';
import { runExpoCliCommand } from '../../utils/project';
export function createPrebuildBuildFunction(): BuildFunction {
return new BuildFunction({
@@ -41,7 +41,6 @@ export function createPrebuildBuildFunction(): BuildFunction {
customPrebuildCommand: job.platform ? job.experimental?.prebuildCommand : undefined,
clean: inputs.clean.value as boolean,
});
const argsWithExpo = ['expo', ...prebuildCommandArgs];
const options = {
cwd: stepCtx.workingDirectory,
logger,
@@ -51,17 +50,7 @@ export function createPrebuildBuildFunction(): BuildFunction {
...(appleTeamId ? { APPLE_TEAM_ID: appleTeamId } : {}),
},
};
if (packageManager === PackageManager.NPM) {
await spawn('npx', argsWithExpo, options);
} else if (packageManager === PackageManager.YARN) {
await spawn('yarn', argsWithExpo, options);
} else if (packageManager === PackageManager.PNPM) {
await spawn('pnpm', argsWithExpo, options);
} else if (packageManager === PackageManager.BUN) {
await spawn('bun', argsWithExpo, options);
} else {
throw new Error(`Unsupported package manager: ${packageManager}`);
}
await runExpoCliCommand({ packageManager, args: prebuildCommandArgs, options });
await installNodeModules(stepCtx, env);
},
});
@@ -28,6 +28,8 @@ jest.mock('../packageManager', () => ({
describe(runExpoCliCommand, () => {
describe('Expo SDK >= 46', () => {
const logger = {} as any;
it('spawns expo via "npx" when package manager is npm', () => {
const mockExpoConfig = mock<ExpoConfig>();
when(mockExpoConfig.sdkVersion).thenReturn('46.0.0');
@@ -38,8 +40,16 @@ describe(runExpoCliCommand, () => {
when(mockCtx.appConfig).thenReturn(Promise.resolve(expoConfig));
const ctx = instance(mockCtx);
void runExpoCliCommand({ args: ['doctor'], options: {}, packageManager: ctx.packageManager });
expect(spawn).toHaveBeenCalledWith('npx', ['expo', 'doctor'], expect.any(Object));
void runExpoCliCommand({
args: ['doctor'],
options: { logger },
packageManager: ctx.packageManager,
});
expect(spawn).toHaveBeenCalledWith(
'npx',
['expo', 'doctor'],
expect.objectContaining({ logger, stdio: ['ignore', 'pipe', 'pipe'] })
);
});
it('spawns expo via "yarn" when package manager is yarn', () => {
@@ -48,12 +58,20 @@ describe(runExpoCliCommand, () => {
const expoConfig = instance(mockExpoConfig);
const mockCtx = mock<BuildContext<Android.Job>>();
when(mockCtx.packageManager).thenReturn(PackageManager.NPM);
when(mockCtx.packageManager).thenReturn(PackageManager.YARN);
when(mockCtx.appConfig).thenReturn(Promise.resolve(expoConfig));
const ctx = instance(mockCtx);
void runExpoCliCommand({ args: ['doctor'], options: {}, packageManager: ctx.packageManager });
expect(spawn).toHaveBeenCalledWith('npx', ['expo', 'doctor'], expect.any(Object));
void runExpoCliCommand({
args: ['doctor'],
options: { logger },
packageManager: ctx.packageManager,
});
expect(spawn).toHaveBeenCalledWith(
'yarn',
['expo', 'doctor'],
expect.objectContaining({ logger, stdio: ['ignore', 'pipe', 'pipe'] })
);
});
it('spawns expo via "pnpm" when package manager is pnpm', () => {
@@ -66,8 +84,16 @@ describe(runExpoCliCommand, () => {
when(mockCtx.appConfig).thenReturn(Promise.resolve(expoConfig));
const ctx = instance(mockCtx);
void runExpoCliCommand({ args: ['doctor'], options: {}, packageManager: ctx.packageManager });
expect(spawn).toHaveBeenCalledWith('pnpm', ['expo', 'doctor'], expect.any(Object));
void runExpoCliCommand({
args: ['doctor'],
options: { logger },
packageManager: ctx.packageManager,
});
expect(spawn).toHaveBeenCalledWith(
'pnpm',
['expo', 'doctor'],
expect.objectContaining({ logger, stdio: ['ignore', 'pipe', 'pipe'] })
);
});
it('spawns expo via "bun" when package manager is bun', () => {
@@ -80,8 +106,30 @@ describe(runExpoCliCommand, () => {
when(mockCtx.appConfig).thenReturn(Promise.resolve(expoConfig));
const ctx = instance(mockCtx);
void runExpoCliCommand({ args: ['doctor'], options: {}, packageManager: ctx.packageManager });
expect(spawn).toHaveBeenCalledWith('bun', ['expo', 'doctor'], expect.any(Object));
void runExpoCliCommand({
args: ['doctor'],
options: { logger },
packageManager: ctx.packageManager,
});
expect(spawn).toHaveBeenCalledWith(
'bun',
['expo', 'doctor'],
expect.objectContaining({ logger, stdio: ['ignore', 'pipe', 'pipe'] })
);
});
it('always pipes stdout and stderr while ignoring stdin', () => {
void runExpoCliCommand({
args: ['doctor'],
options: { logger },
packageManager: PackageManager.NPM,
});
expect(spawn).toHaveBeenCalledWith(
'npx',
['expo', 'doctor'],
expect.objectContaining({ logger, stdio: ['ignore', 'pipe', 'pipe'] })
);
});
});
});
+10 -5
View File
@@ -1,4 +1,5 @@
import spawn, { SpawnOptions, SpawnPromise, SpawnResult } from '@expo/turtle-spawn';
import { bunyan } from '@expo/logger';
import fs from 'fs-extra';
import path from 'path';
@@ -50,17 +51,21 @@ export function runExpoCliCommand({
}: {
packageManager: PackageManager;
args: string[];
options: SpawnOptions;
options: Omit<SpawnOptions, 'stdio' | 'logger'> & { logger: bunyan };
}): SpawnPromise<SpawnResult> {
const normalizedOptions: SpawnOptions = {
...options,
stdio: ['ignore', 'pipe', 'pipe'],
};
const argsWithExpo = ['expo', ...args];
if (packageManager === PackageManager.NPM) {
return spawn('npx', argsWithExpo, options);
return spawn('npx', argsWithExpo, normalizedOptions);
} else if (packageManager === PackageManager.YARN) {
return spawn('yarn', argsWithExpo, options);
return spawn('yarn', argsWithExpo, normalizedOptions);
} else if (packageManager === PackageManager.PNPM) {
return spawn('pnpm', argsWithExpo, options);
return spawn('pnpm', argsWithExpo, normalizedOptions);
} else if (packageManager === PackageManager.BUN) {
return spawn('bun', argsWithExpo, options);
return spawn('bun', argsWithExpo, normalizedOptions);
} else {
throw new Error(`Unsupported package manager: ${packageManager}`);
}
@@ -57,6 +57,7 @@ async function getExpoConfigInternalAsync(
projectDir,
['config', '--json', ...(opts.isPublicConfig ? ['--type', 'public'] : [])],
{
stdio: ['ignore', 'pipe', 'pipe'],
env: {
EXPO_NO_DOTENV: '1',
},
@@ -93,7 +93,10 @@ it('uses the bundled config fallback when the local Expo CLI fails', async () =>
expect(spawnExpoCommand).toHaveBeenCalledWith(
projectDir,
['config', '--json', '--type', 'introspect'],
{ env: expect.objectContaining({ EXPO_NO_DOTENV: '1' }) }
{
stdio: ['ignore', 'pipe', 'pipe'],
env: expect.objectContaining({ EXPO_NO_DOTENV: '1' }),
}
);
expect(getPrebuildConfigAsync).toHaveBeenCalledWith(projectDir, { platforms: ['ios'] });
});
@@ -28,6 +28,7 @@ export async function getManagedApplicationTargetEntitlementsAsync(
projectDir,
['config', '--json', '--type', 'introspect'],
{
stdio: ['ignore', 'pipe', 'pipe'],
env: {
...env,
EXPO_NO_DOTENV: '1',
@@ -0,0 +1,30 @@
import spawnAsync from '@expo/spawn-async';
import { pipeSpawnOutput } from '@expo/logger';
import spawn from '../index';
jest.mock('@expo/spawn-async', () => jest.fn(() => ({ child: {} })));
jest.mock('@expo/logger', () => ({
pipeSpawnOutput: jest.fn(),
}));
describe('spawn', () => {
it('defaults stdio to pipe when a logger is provided', () => {
const logger = {} as any;
void spawn('echo', ['hello'], { logger });
expect(spawnAsync).toHaveBeenCalledWith('echo', ['hello'], { stdio: 'pipe' });
expect(pipeSpawnOutput).toHaveBeenCalledWith(logger, {}, { stdio: 'pipe' });
});
it('preserves explicit stdio when a logger is provided', () => {
const logger = {} as any;
const stdio: ['ignore', 'pipe', 'pipe'] = ['ignore', 'pipe', 'pipe'];
void spawn('echo', ['hello'], { logger, stdio });
expect(spawnAsync).toHaveBeenCalledWith('echo', ['hello'], { stdio });
expect(pipeSpawnOutput).toHaveBeenCalledWith(logger, {}, { stdio });
});
});
+1 -1
View File
@@ -19,7 +19,7 @@ function spawn(
}
): SpawnPromise<SpawnResult> {
const { logger, ...options } = _options;
if (logger) {
if (logger && options.stdio === undefined) {
options.stdio = 'pipe';
}
const promise = spawnAsync(command, args, options);
+1
View File
@@ -9,6 +9,7 @@
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"types": ["node", "jest"],
"composite": true,
"outDir": "./dist",
"rootDir": "./src"