Skip to content

Commit 4c0489c

Browse files
committed
feat: add commands to open Android Emulator and iOS Simulator
* Added `openEmulator` and `openSimulator` functions to manage emulator and simulator processes. * Updated dependencies to include `@clack/prompts` for user prompts.
1 parent 279afee commit 4c0489c

8 files changed

Lines changed: 202 additions & 1 deletion

File tree

packages/repack/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
}
7676
},
7777
"dependencies": {
78+
"@clack/prompts": "^0.9.1",
7879
"@callstack/repack-dev-server": "workspace:*",
7980
"@discoveryjs/json-ext": "^0.5.7",
8081
"@rspack/plugin-react-refresh": "1.0.0",

packages/repack/src/commands/common/__tests__/setupInteractions.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,8 @@ describe('setupInteractions', () => {
245245
onOpenDevMenu() {},
246246
onReload() {},
247247
onAdbReverse() {},
248+
onOpenEmulator() {},
249+
onOpenSimulator() {},
248250
},
249251
{
250252
logger: mockLogger,
@@ -271,6 +273,14 @@ describe('setupInteractions', () => {
271273
);
272274
expect(mockProcess.stdout.write).toHaveBeenNthCalledWith(
273275
5,
276+
' e: Open Android Emulator\n'
277+
);
278+
expect(mockProcess.stdout.write).toHaveBeenNthCalledWith(
279+
6,
280+
' s: Open iOS Simulator\n'
281+
);
282+
expect(mockProcess.stdout.write).toHaveBeenNthCalledWith(
283+
7,
274284
'\nPress Ctrl+c or Ctrl+z to quit the dev server\n\n'
275285
);
276286
});
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { execSync, spawn } from 'node:child_process';
2+
3+
function getRunningEmulators(): string[] {
4+
try {
5+
const adbOutput = execSync('adb devices').toString();
6+
const lines = adbOutput.split('\n').slice(1); // Skip the first line (header)
7+
return lines
8+
.map((line) => {
9+
const match = line.match(/emulator-(\d+)/);
10+
if (match) {
11+
// Get the AVD name for this port
12+
try {
13+
const port = match[1];
14+
const avdInfo = execSync(`adb -s emulator-${port} emu avd name`)
15+
.toString()
16+
.replace('OK', '')
17+
.trim();
18+
return avdInfo;
19+
} catch {
20+
return null;
21+
}
22+
}
23+
return null;
24+
})
25+
.filter((name): name is string => name !== null);
26+
} catch {
27+
return [];
28+
}
29+
}
30+
31+
export async function openEmulator() {
32+
const emulator = execSync('emulator -list-avds');
33+
const avds = emulator.toString().split('\n').filter(Boolean);
34+
const avd = avds?.[0];
35+
if (!avd) {
36+
throw new Error('No Android Virtual Device found');
37+
}
38+
39+
const { select, isCancel, log } = await import('@clack/prompts');
40+
41+
const runningEmulators = getRunningEmulators();
42+
43+
if (runningEmulators.length > 0) {
44+
log.info('Running emulators:');
45+
runningEmulators.map((avd) => {
46+
log.success(`${avd} is running`);
47+
});
48+
log.info('');
49+
}
50+
51+
// show prompt to select avd use @clack/prompts
52+
const selectedAvd = await select({
53+
message: 'Select an Android Virtual Device',
54+
options: avds
55+
.filter((avd) => !runningEmulators.includes(avd))
56+
.map((avd) => {
57+
return {
58+
label: avd,
59+
value: avd,
60+
};
61+
}),
62+
});
63+
64+
if (isCancel(selectedAvd)) {
65+
throw new Error('No Android Virtual Device selected');
66+
}
67+
68+
// Spawn emulator process with proper detachment
69+
const emulatorProcess = spawn('emulator', ['-avd', selectedAvd.toString()], {
70+
detached: true,
71+
stdio: 'ignore',
72+
});
73+
74+
log.success(`${selectedAvd} is running`);
75+
76+
// Unref the process to allow the parent to exit independently
77+
emulatorProcess.unref();
78+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { execSync } from 'node:child_process';
2+
3+
interface Simulator {
4+
name: string;
5+
udid: string;
6+
state: string;
7+
isAvailable: boolean;
8+
}
9+
10+
export async function openSimulator() {
11+
try {
12+
// Get list of available simulators
13+
const devices = execSync(
14+
'xcrun simctl list devices available --json'
15+
).toString();
16+
const parsedDevices = JSON.parse(devices);
17+
const runtimes = Object.keys(parsedDevices.devices);
18+
19+
// Collect all available simulators
20+
const availableSimulators: Simulator[] = [];
21+
const runningSimulators: Simulator[] = [];
22+
23+
for (const runtime of runtimes) {
24+
const simulators = parsedDevices.devices[runtime];
25+
simulators.forEach((sim: any) => {
26+
if (sim.isAvailable !== false) {
27+
// Add if simulator is available
28+
const simulator = {
29+
name: `${sim.name} (${runtime})`,
30+
udid: sim.udid,
31+
state: sim.state,
32+
isAvailable: true,
33+
};
34+
35+
if (sim.state !== 'Shutdown') {
36+
runningSimulators.push(simulator);
37+
} else {
38+
availableSimulators.push(simulator);
39+
}
40+
}
41+
});
42+
}
43+
44+
const { select, isCancel, log } = await import('@clack/prompts');
45+
46+
// Log running simulators
47+
if (runningSimulators.length > 0) {
48+
log.info('Running simulators:');
49+
runningSimulators.forEach((sim) => {
50+
log.success(` • ${sim.name}`);
51+
});
52+
log.info(''); // Empty line for better readability
53+
}
54+
55+
if (availableSimulators.length === 0) {
56+
throw new Error('No available (shutdown) iOS Simulators found');
57+
}
58+
59+
// Show prompt to select simulator (only showing shutdown simulators)
60+
const selectedSimulator = await select({
61+
message: 'Select an iOS Simulator',
62+
options: availableSimulators.map((sim) => ({
63+
label: sim.name,
64+
value: sim.udid,
65+
})),
66+
});
67+
68+
if (isCancel(selectedSimulator)) {
69+
throw new Error('No iOS Simulator selected');
70+
}
71+
72+
// Boot the simulator
73+
execSync(`xcrun simctl boot ${selectedSimulator}`);
74+
// Open Simulator.app
75+
execSync('open -a Simulator');
76+
} catch (error) {
77+
if (error instanceof Error) {
78+
throw new Error(`Failed to open iOS Simulator: ${error.message}`);
79+
}
80+
throw error;
81+
}
82+
}

packages/repack/src/commands/common/setupInteractions.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export function setupInteractions(
2222
onOpenDevMenu?: () => void;
2323
onOpenDevTools?: () => void;
2424
onAdbReverse?: () => void;
25+
onOpenEmulator?: () => void;
26+
onOpenSimulator?: () => void;
2527
},
2628
options?: {
2729
logger?: Logger;
@@ -103,6 +105,16 @@ export function setupInteractions(
103105
postPerformMessage: 'Running adb reverse',
104106
helpName: 'Run adb reverse',
105107
},
108+
e: {
109+
action: handlers.onOpenEmulator,
110+
postPerformMessage: 'Opening Android Emulator',
111+
helpName: 'Open Android Emulator',
112+
},
113+
s: {
114+
action: handlers.onOpenSimulator,
115+
postPerformMessage: 'Opening iOS Simulator',
116+
helpName: 'Open iOS Simulator',
117+
},
106118
};
107119

108120
// use process.stdout for sync output at startup

packages/repack/src/commands/rspack/start.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ import {
1717
} from '../common/index.js';
1818
import { runAdbReverse } from '../common/index.js';
1919
import logo from '../common/logo.js';
20+
import { openEmulator } from '../common/openEmulator.js';
21+
import { openSimulator } from '../common/openSimulator.js';
2022
import { setupEnvironment } from '../common/setupEnvironment.js';
2123
import type { CliConfig, StartArguments } from '../types.js';
2224
import { Compiler } from './Compiler.js';
23-
2425
/**
2526
* Start command that runs a development server.
2627
* It runs `@callstack/repack-dev-server` to provide Development Server functionality
@@ -115,6 +116,12 @@ export async function start(
115116
verbose: true,
116117
});
117118
},
119+
onOpenEmulator() {
120+
void openEmulator();
121+
},
122+
onOpenSimulator() {
123+
void openSimulator();
124+
},
118125
},
119126
{ logger: ctx.log }
120127
);

packages/repack/src/commands/webpack/start.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {
1919
setupInteractions,
2020
} from '../common/index.js';
2121
import logo from '../common/logo.js';
22+
import { openEmulator } from '../common/openEmulator.js';
23+
import { openSimulator } from '../common/openSimulator.js';
2224
import { setupEnvironment } from '../common/setupEnvironment.js';
2325
import type { CliConfig, StartArguments } from '../types.js';
2426
import { Compiler } from './Compiler.js';
@@ -113,6 +115,12 @@ export async function start(
113115
verbose: true,
114116
});
115117
},
118+
onOpenEmulator() {
119+
void openEmulator();
120+
},
121+
onOpenSimulator() {
122+
void openSimulator();
123+
},
116124
},
117125
{ logger: ctx.log }
118126
);

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)