mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 17:50:32 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7e5d7c7ee | ||
|
|
657e6f27a6 | ||
|
|
fea675c1e4 |
No files matched your search
@@ -10,6 +10,7 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
- [eas-cli] Add Supabase integration foundation: GraphQL client, shared integration helpers, and provisioning utilities. ([#4130](https://github.com/expo/eas-cli/pull/4130) by [@gwdp](https://github.com/gwdp))
|
||||
- [eas-cli] Add `eas integrations:supabase:connect` command. ([#4106](https://github.com/expo/eas-cli/pull/4106) by [@gwdp](https://github.com/gwdp))
|
||||
- [eas-cli] Add `eas simulator:feedback` to send EAS Simulator feedback to the Expo team through `submit-expo-feedback`, attaching the active session ID. ([#4176](https://github.com/expo/eas-cli/pull/4176) by [@zvadaadam](https://github.com/zvadaadam))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import { Config } from '@oclif/core';
|
||||
|
||||
import { EAS_SIMULATOR_SESSION_ID } from '../../../simulator/env';
|
||||
import SimulatorFeedback from '../feedback';
|
||||
|
||||
jest.mock('@expo/spawn-async');
|
||||
|
||||
const mockSpawnAsync = jest.mocked(spawnAsync);
|
||||
|
||||
function getMockOclifConfig(): Config {
|
||||
const config = new Config({ root: __dirname });
|
||||
config.runHook = async () => ({
|
||||
failures: [],
|
||||
successes: [],
|
||||
});
|
||||
return config;
|
||||
}
|
||||
|
||||
describe(SimulatorFeedback, () => {
|
||||
const mockConfig = getMockOclifConfig();
|
||||
const projectDir = '/test/project';
|
||||
const previousDeviceRunSessionId = process.env[EAS_SIMULATOR_SESSION_ID];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
delete process.env[EAS_SIMULATOR_SESSION_ID];
|
||||
mockSpawnAsync.mockResolvedValue({} as never);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (previousDeviceRunSessionId === undefined) {
|
||||
delete process.env[EAS_SIMULATOR_SESSION_ID];
|
||||
} else {
|
||||
process.env[EAS_SIMULATOR_SESSION_ID] = previousDeviceRunSessionId;
|
||||
}
|
||||
});
|
||||
|
||||
function createCommand(argv: string[]): {
|
||||
command: SimulatorFeedback;
|
||||
getContextAsync: jest.SpyInstance;
|
||||
} {
|
||||
const command = new SimulatorFeedback(argv, mockConfig);
|
||||
// @ts-expect-error getContextAsync is protected
|
||||
const getContextAsync = jest.spyOn(command, 'getContextAsync').mockResolvedValue({
|
||||
projectDir,
|
||||
});
|
||||
return { command, getContextAsync };
|
||||
}
|
||||
|
||||
it('submits feedback through submit-expo-feedback with the simulator category', async () => {
|
||||
const { command } = createCommand([
|
||||
'Sessions boot fast, but exec feels slow',
|
||||
'--non-interactive',
|
||||
]);
|
||||
await command.runAsync();
|
||||
|
||||
expect(mockSpawnAsync).toHaveBeenCalledWith(
|
||||
'npx',
|
||||
[
|
||||
'--yes',
|
||||
'submit-expo-feedback@latest',
|
||||
'--category',
|
||||
'simulator',
|
||||
'Sessions boot fast, but exec feels slow',
|
||||
],
|
||||
expect.objectContaining({
|
||||
cwd: projectDir,
|
||||
stdio: 'inherit',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it(`passes an inherited ${EAS_SIMULATOR_SESSION_ID} through to the subprocess`, async () => {
|
||||
process.env[EAS_SIMULATOR_SESSION_ID] = 'session-from-env';
|
||||
|
||||
const { command } = createCommand(['Great product', '--non-interactive']);
|
||||
await command.runAsync();
|
||||
|
||||
const spawnOptions = mockSpawnAsync.mock.calls[0][2]!;
|
||||
expect(spawnOptions.env?.[EAS_SIMULATOR_SESSION_ID]).toBe('session-from-env');
|
||||
});
|
||||
|
||||
it('prefers --id over an inherited session variable', async () => {
|
||||
process.env[EAS_SIMULATOR_SESSION_ID] = 'session-from-env';
|
||||
|
||||
const { command } = createCommand([
|
||||
'Great product',
|
||||
'--id',
|
||||
'session-from-flag',
|
||||
'--non-interactive',
|
||||
]);
|
||||
await command.runAsync();
|
||||
|
||||
const spawnOptions = mockSpawnAsync.mock.calls[0][2]!;
|
||||
expect(spawnOptions.env?.[EAS_SIMULATOR_SESSION_ID]).toBe('session-from-flag');
|
||||
});
|
||||
|
||||
it('passes --subject through to submit-expo-feedback', async () => {
|
||||
const { command } = createCommand([
|
||||
'The exec command needs a quieter output mode',
|
||||
'--subject',
|
||||
'simulator:exec',
|
||||
'--non-interactive',
|
||||
]);
|
||||
await command.runAsync();
|
||||
|
||||
expect(mockSpawnAsync).toHaveBeenCalledWith(
|
||||
'npx',
|
||||
[
|
||||
'--yes',
|
||||
'submit-expo-feedback@latest',
|
||||
'--category',
|
||||
'simulator',
|
||||
'--subject',
|
||||
'simulator:exec',
|
||||
'The exec command needs a quieter output mode',
|
||||
],
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it('does not leak an unset session variable into the subprocess env', async () => {
|
||||
const { command } = createCommand(['Great product', '--non-interactive']);
|
||||
await command.runAsync();
|
||||
|
||||
const spawnOptions = mockSpawnAsync.mock.calls[0][2]!;
|
||||
expect(spawnOptions.env?.[EAS_SIMULATOR_SESSION_ID]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('throws a helpful error when no message is passed in non-interactive mode', async () => {
|
||||
const { command } = createCommand(['--non-interactive']);
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
'Feedback message is required in non-interactive mode. Run `eas simulator:feedback "<your feedback>"` to pass it as an argument.'
|
||||
);
|
||||
expect(mockSpawnAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('spawns without a message in interactive mode so submit-expo-feedback prompts for it', async () => {
|
||||
const previousIsTTY = process.stdin.isTTY;
|
||||
const previousCI = process.env.CI;
|
||||
process.stdin.isTTY = true;
|
||||
delete process.env.CI;
|
||||
|
||||
try {
|
||||
const { command } = createCommand([]);
|
||||
await command.runAsync();
|
||||
|
||||
expect(mockSpawnAsync).toHaveBeenCalledWith(
|
||||
'npx',
|
||||
['--yes', 'submit-expo-feedback@latest', '--category', 'simulator'],
|
||||
expect.anything()
|
||||
);
|
||||
} finally {
|
||||
process.stdin.isTTY = previousIsTTY;
|
||||
if (previousCI === undefined) {
|
||||
delete process.env.CI;
|
||||
} else {
|
||||
process.env.CI = previousCI;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import { Args, Flags } from '@oclif/core';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { isNonInteractiveByDefault } from '../../commandUtils/flags';
|
||||
import { EAS_SIMULATOR_SESSION_ID, SIMULATOR_DOTENV_FILE_NAME } from '../../simulator/env';
|
||||
|
||||
export default class SimulatorFeedback extends EasCommand {
|
||||
static override hidden = true;
|
||||
static override aliases = ['sim:feedback'];
|
||||
static override description = '[EXPERIMENTAL] send feedback about EAS Simulator to the Expo team';
|
||||
|
||||
static override args = {
|
||||
message: Args.string({
|
||||
description: 'Feedback message. Omit it to be prompted in an interactive terminal.',
|
||||
}),
|
||||
};
|
||||
|
||||
static override flags = {
|
||||
id: Flags.string({
|
||||
description: `Simulator session ID to attach to the feedback. Defaults to the session in ${SIMULATOR_DOTENV_FILE_NAME}.`,
|
||||
}),
|
||||
subject: Flags.string({
|
||||
description: 'Simulator command or feature the feedback is about, e.g. "simulator:exec".',
|
||||
}),
|
||||
'non-interactive': Flags.boolean({
|
||||
description: 'Run the command in non-interactive mode.',
|
||||
default: () => Promise.resolve(isNonInteractiveByDefault()),
|
||||
noCacheDefault: true,
|
||||
}),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectDir,
|
||||
};
|
||||
|
||||
private isRunningSubprocess = false;
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { args, flags } = await this.parse(SimulatorFeedback);
|
||||
const nonInteractive = flags['non-interactive'];
|
||||
|
||||
const message = args.message?.trim();
|
||||
if (nonInteractive && !message) {
|
||||
throw new Error(
|
||||
'Feedback message is required in non-interactive mode. Run `eas simulator:feedback "<your feedback>"` to pass it as an argument.'
|
||||
);
|
||||
}
|
||||
|
||||
const { projectDir } = await this.getContextAsync(SimulatorFeedback, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
// submit-expo-feedback resolves the active session itself, from EAS_SIMULATOR_SESSION_ID
|
||||
// or the simulator dotenv file in its working directory; --id overrides it through the
|
||||
// environment. It also prompts for the message when one is not passed.
|
||||
this.isRunningSubprocess = true;
|
||||
await spawnAsync(
|
||||
'npx',
|
||||
[
|
||||
'--yes',
|
||||
'submit-expo-feedback@latest',
|
||||
'--category',
|
||||
'simulator',
|
||||
...(flags.subject ? ['--subject', flags.subject] : []),
|
||||
...(message ? [message] : []),
|
||||
],
|
||||
{
|
||||
cwd: projectDir,
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
...(flags.id ? { [EAS_SIMULATOR_SESSION_ID]: flags.id } : {}),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected override catch(err: Error): Promise<any> {
|
||||
// Propagate wrapped command from spawnAsync rejection
|
||||
if (this.isRunningSubprocess) {
|
||||
process.exitCode = process.exitCode ?? (err as any).status ?? 1;
|
||||
return Promise.resolve();
|
||||
}
|
||||
return super.catch(err);
|
||||
}
|
||||
}
|
||||
@@ -55,8 +55,10 @@ const APP_PLATFORM_BY_FLAG_VALUE: Record<PlatformFlagValue, AppPlatform> = {
|
||||
export default class Simulator extends EasCommand {
|
||||
static override hidden = true;
|
||||
static override aliases = ['simulator:start', 'sim', 'sim:start'];
|
||||
static override description =
|
||||
static override summary =
|
||||
'[EXPERIMENTAL] start a remote simulator session on EAS and get instructions to connect to it';
|
||||
static override description =
|
||||
'Send feedback about EAS Simulator with `eas simulator:feedback "<message>"`.';
|
||||
|
||||
static override flags = {
|
||||
platform: Flags.option({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import {
|
||||
@@ -6,6 +7,7 @@ import {
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import { DeviceRunSessionMutation } from '../../graphql/mutations/DeviceRunSessionMutation';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import {
|
||||
EAS_SIMULATOR_SESSION_ID,
|
||||
@@ -70,6 +72,8 @@ export default class SimulatorStop extends EasCommand {
|
||||
|
||||
if (jsonFlag) {
|
||||
printJsonOnlyOutput({ id: session.id, status: session.status });
|
||||
} else {
|
||||
Log.log(chalk.dim('Have feedback about EAS Simulator? Run `eas simulator:feedback`.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user