diff --git a/.changeset/little-rocks-mix.md b/.changeset/little-rocks-mix.md new file mode 100644 index 000000000..e4f3cc0f9 --- /dev/null +++ b/.changeset/little-rocks-mix.md @@ -0,0 +1,10 @@ +--- +'@rock-js/platform-apple-helpers': patch +'@rock-js/platform-android': patch +'@rock-js/plugin-repack': patch +'@rock-js/platform-ios': patch +'@rock-js/plugin-metro': patch +'@rock-js/config': patch +--- + +feat: allow running dev server from run commands with `--dev-server` flag diff --git a/packages/config/src/lib/config.ts b/packages/config/src/lib/config.ts index 27762ba4c..c433b7bcd 100644 --- a/packages/config/src/lib/config.ts +++ b/packages/config/src/lib/config.ts @@ -13,6 +13,34 @@ export type PluginOutput = { description: string; }; +export type DevServerArgs = { + interactive: boolean; + clientLogs: boolean; + port?: string; + host?: string; + https?: boolean; + resetCache?: boolean; + devServer?: boolean; + platforms?: string[]; + [key: string]: unknown; +}; + +export type StartDevServerArgs = { + root: string; + args: DevServerArgs; + reactNativeVersion: string; + reactNativePath: string; + platforms: Record; +}; + +type StartDevServerFunction = (options: StartDevServerArgs) => Promise; + +export type BundlerPluginOutput = { + name: string; + description: string; + start: StartDevServerFunction; +}; + export type PlatformOutput = PluginOutput & { autolinkingConfig: { project: Record | undefined }; }; @@ -27,10 +55,11 @@ export type PluginApi = { null | undefined | (() => RemoteBuildCache) >; getFingerprintOptions: () => FingerprintSources; + getBundlerStart: () => ({ args }: { args: DevServerArgs }) => void; }; type PluginType = (args: PluginApi) => PluginOutput; - +type BundlerPluginType = (args: PluginApi) => BundlerPluginOutput; type PlatformType = (args: PluginApi) => PlatformOutput; type ArgValue = string | string[] | boolean; @@ -63,7 +92,7 @@ export type ConfigType = { root?: string; reactNativeVersion?: string; reactNativePath?: string; - bundler?: PluginType; + bundler?: BundlerPluginType; plugins?: PluginType[]; platforms?: Record; commands?: Array; @@ -79,6 +108,7 @@ export type ConfigOutput = { root: string; commands?: Array; platforms?: Record; + bundler?: BundlerPluginOutput; } & PluginApi; const extensions = ['.js', '.ts', '.mjs']; @@ -160,6 +190,8 @@ export async function getConfig( process.exit(1); } + let bundler: BundlerPluginOutput | undefined; + const api = { registerCommand: (command: CommandType) => { validatedConfig.commands = [...(validatedConfig.commands || []), command]; @@ -186,6 +218,17 @@ Read more: ${colorLink('https://rockjs.dev/docs/configuration#github-actions-pro }, getFingerprintOptions: () => validatedConfig.fingerprint as FingerprintSources, + getBundlerStart: + () => + ({ args }: { args: DevServerArgs }) => { + return bundler?.start({ + root: api.getProjectRoot(), + args, + reactNativeVersion: api.getReactNativeVersion(), + reactNativePath: api.getReactNativePath(), + platforms: api.getPlatforms(), + }); + }, }; const platforms: Record = {}; @@ -205,7 +248,11 @@ Read more: ${colorLink('https://rockjs.dev/docs/configuration#github-actions-pro } if (validatedConfig.bundler) { - assignOriginToCommand(validatedConfig.bundler, api, validatedConfig); + bundler = assignOriginToCommand( + validatedConfig.bundler, + api, + validatedConfig, + ) as BundlerPluginOutput; } for (const internalPlugin of internalPlugins) { @@ -220,6 +267,7 @@ Read more: ${colorLink('https://rockjs.dev/docs/configuration#github-actions-pro root: projectRoot, commands: validatedConfig.commands ?? [], platforms: platforms ?? {}, + bundler, ...api, }; @@ -236,16 +284,17 @@ function resolveReactNativePath(root: string) { * Assigns __origin property to each command in the config for later use in error handling. */ function assignOriginToCommand( - plugin: PluginType, + plugin: PluginType | BundlerPluginType, api: PluginApi, config: ConfigType, ) { const len = config.commands?.length ?? 0; - const { name } = plugin(api); + const { name, ...rest } = plugin(api); const newlen = config.commands?.length ?? 0; for (let i = len; i < newlen; i++) { if (config.commands?.[i]) { config.commands[i].__origin = name; } } + return { name, ...rest }; } diff --git a/packages/platform-android/src/lib/commands/runAndroid/__tests__/runAndroid.test.ts b/packages/platform-android/src/lib/commands/runAndroid/__tests__/runAndroid.test.ts index d846306b0..b42a62945 100644 --- a/packages/platform-android/src/lib/commands/runAndroid/__tests__/runAndroid.test.ts +++ b/packages/platform-android/src/lib/commands/runAndroid/__tests__/runAndroid.test.ts @@ -32,6 +32,8 @@ const androidProject: AndroidProjectConfig = { const OLD_ENV = process.env; let adbDevicesCallsCount = 0; +const mockPlatforms = { ios: {}, android: {} }; + beforeEach(() => { adbDevicesCallsCount = 0; vi.clearAllMocks(); @@ -312,9 +314,12 @@ test.each([['release'], ['debug'], ['staging']])( '/', undefined, { extraSources: [], ignorePaths: [], env: [] }, + vi.fn(), // startDevServer mock + '/path/to/react-native', // reactNativePath + '0.79.0', // reactNativeVersion + mockPlatforms, ); - expect(tools.outro).toBeCalledWith('Success 🎉.'); expect(tools.logger.error).not.toBeCalled(); // Runs installDebug with only active architecture arm64-v8a @@ -361,9 +366,12 @@ test('runAndroid runs gradle build with custom --appId, --appIdSuffix and --main '/', undefined, { extraSources: [], ignorePaths: [], env: [] }, + vi.fn(), // startDevServer mock + '/path/to/react-native', // reactNativePath + '0.79.0', // reactNativeVersion + mockPlatforms, ); - expect(tools.outro).toBeCalledWith('Success 🎉.'); expect(logErrorSpy).not.toBeCalled(); // launches com.custom.suffix app with OtherActivity on emulator-5552 @@ -388,6 +396,10 @@ test('runAndroid fails to launch an app on not-connected device when specified w '/', undefined, { extraSources: [], ignorePaths: [], env: [] }, + vi.fn(), // startDevServer mock + '/path/to/react-native', // reactNativePath + '0.79.0', // reactNativeVersion + mockPlatforms, ); expect(logWarnSpy).toBeCalledWith( 'No devices or emulators found matching "emulator-5554". Using available one instead.', @@ -457,6 +469,10 @@ test.each([['release'], ['debug']])( '/', undefined, { extraSources: [], ignorePaths: [], env: [] }, + vi.fn(), // startDevServer mock + '/path/to/react-native', // reactNativePath + '0.79.0', // reactNativeVersion + mockPlatforms, ); // we don't want to run installDebug when a device is selected, because gradle will install the app on all connected devices @@ -513,11 +529,21 @@ test('runAndroid launches an app on all connected devices', async () => { }); }); - await runAndroid({ ...androidProject }, { ...args }, '/', undefined, { - extraSources: [], - ignorePaths: [], - env: [], - }); + await runAndroid( + { ...androidProject }, + { ...args }, + '/', + undefined, + { + extraSources: [], + ignorePaths: [], + env: [], + }, + vi.fn(), + '/path/to/react-native', + '0.79.0', + mockPlatforms, + ); // Runs assemble debug task with active architectures arm64-v8a, armeabi-v7a expect(spawn).toBeCalledWith( @@ -584,6 +610,10 @@ test('runAndroid skips building when --binary-path is passed', async () => { '/root', undefined, { extraSources: [], ignorePaths: [], env: [] }, + vi.fn(), // startDevServer mock + '/path/to/react-native', // reactNativePath + '0.79.0', // reactNativeVersion + mockPlatforms, ); // Skips gradle diff --git a/packages/platform-android/src/lib/commands/runAndroid/command.ts b/packages/platform-android/src/lib/commands/runAndroid/command.ts index 69e7646df..98d5573bd 100644 --- a/packages/platform-android/src/lib/commands/runAndroid/command.ts +++ b/packages/platform-android/src/lib/commands/runAndroid/command.ts @@ -1,5 +1,6 @@ import type { AndroidProjectConfig } from '@react-native-community/cli-types'; import type { PluginApi } from '@rock-js/config'; +import { intro, outro } from '@rock-js/tools'; import { getValidProjectConfig } from '../getValidProjectConfig.js'; import type { Flags } from './runAndroid.js'; import { runAndroid, runOptions } from './runAndroid.js'; @@ -13,6 +14,7 @@ export function registerRunCommand( description: 'Builds your app and starts it on a connected Android emulator or a device.', action: async (args) => { + intro('Running Android app'); const projectRoot = api.getProjectRoot(); const androidConfig = getValidProjectConfig(projectRoot, pluginConfig); await runAndroid( @@ -21,7 +23,12 @@ export function registerRunCommand( projectRoot, await api.getRemoteCacheProvider(), api.getFingerprintOptions(), + api.getBundlerStart(), + api.getReactNativeVersion(), + api.getReactNativePath(), + api.getPlatforms(), ); + outro('Success 🎉.'); }, options: runOptions, }); diff --git a/packages/platform-android/src/lib/commands/runAndroid/runAndroid.ts b/packages/platform-android/src/lib/commands/runAndroid/runAndroid.ts index 88241f5ea..f3c70e1a2 100644 --- a/packages/platform-android/src/lib/commands/runAndroid/runAndroid.ts +++ b/packages/platform-android/src/lib/commands/runAndroid/runAndroid.ts @@ -4,14 +4,13 @@ import type { AndroidProjectConfig, Config, } from '@react-native-community/cli-types'; +import type { StartDevServerArgs } from '@rock-js/config'; import type { FingerprintSources, RemoteBuildCache } from '@rock-js/tools'; import { color, formatArtifactName, - intro, isInteractive, logger, - outro, promptSelect, RockError, spinner, @@ -37,6 +36,8 @@ export interface Flags extends BuildFlags { binaryPath?: string; user?: string; local?: boolean; + devServer?: boolean; + clientLogs?: boolean; } export type AndroidProject = NonNullable; @@ -50,8 +51,26 @@ export async function runAndroid( projectRoot: string, remoteCacheProvider: null | (() => RemoteBuildCache) | undefined, fingerprintOptions: FingerprintSources, + startDevServer: (options: StartDevServerArgs) => void, + reactNativeVersion: string, + reactNativePath: string, + platforms: { [platform: string]: object }, ) { - intro('Running Android app'); + const startDevServerHelper = () => { + if (args.devServer) { + logger.info('Starting dev server...'); + startDevServer({ + root: projectRoot, + reactNativePath, + reactNativeVersion, + platforms, + args: { + interactive: isInteractive(), + clientLogs: args.clientLogs ?? true, + }, + }); + } + }; normalizeArgs(args, projectRoot); @@ -111,7 +130,7 @@ export async function runAndroid( } } - outro('Success 🎉.'); + startDevServerHelper(); } async function selectAndLaunchDevice() { @@ -279,4 +298,13 @@ export const runOptions = [ name: '--user ', description: 'Id of the User Profile you want to install the app on.', }, + { + name: '--client-logs', + description: 'Enable client logs in dev server.', + }, + { + name: '--dev-server', + description: + 'Automatically start a dev server (bundler) after building the app.', + }, ]; diff --git a/packages/platform-apple-helpers/src/lib/commands/run/createRun.ts b/packages/platform-apple-helpers/src/lib/commands/run/createRun.ts index dc8abc2e4..cbb11c214 100644 --- a/packages/platform-apple-helpers/src/lib/commands/run/createRun.ts +++ b/packages/platform-apple-helpers/src/lib/commands/run/createRun.ts @@ -1,5 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; +import type { StartDevServerArgs } from '@rock-js/config'; import type { FingerprintSources, RemoteBuildCache } from '@rock-js/tools'; import { color, @@ -34,6 +35,9 @@ export const createRun = async ({ remoteCacheProvider, fingerprintOptions, reactNativePath, + reactNativeVersion, + platforms, + startDevServer, }: { platformName: ApplePlatform; projectConfig: ProjectConfig; @@ -42,7 +46,25 @@ export const createRun = async ({ remoteCacheProvider: null | (() => RemoteBuildCache) | undefined; fingerprintOptions: FingerprintSources; reactNativePath: string; + reactNativeVersion: string; + platforms: { [platform: string]: object }; + startDevServer: (options: StartDevServerArgs) => void; }) => { + const startDevServerHelper = () => { + if (args.devServer) { + logger.info('Starting dev server...'); + startDevServer({ + root: projectRoot, + reactNativePath, + reactNativeVersion, + platforms, + args: { + interactive: isInteractive(), + clientLogs: args.clientLogs ?? true, + }, + }); + } + }; validateArgs(args, projectRoot); const deviceOrSimulator = args.destination @@ -92,7 +114,9 @@ export const createRun = async ({ deviceOrSimulator, fingerprintOptions, }); + await runOnMac(appPath); + startDevServerHelper(); return; } else if (args.catalyst) { const { appPath, scheme } = await buildApp({ @@ -108,8 +132,10 @@ export const createRun = async ({ deviceOrSimulator, fingerprintOptions, }); + if (scheme) { await runOnMacCatalyst(appPath, scheme); + startDevServerHelper(); return; } else { throw new RockError('Failed to get project scheme'); @@ -128,7 +154,7 @@ export const createRun = async ({ if (device) { if (device.type !== deviceOrSimulator) { throw new RockError( - `Selected device "${device.name}" is not a ${deviceOrSimulator}. + `Selected device "${device.name}" is not a ${deviceOrSimulator}. Please either use "--destination ${ deviceOrSimulator === 'simulator' ? 'device' : 'simulator' }" flag or select available ${deviceOrSimulator}: @@ -155,7 +181,9 @@ ${devices fingerprintOptions, }), ]); + await runOnSimulator(device, appPath, infoPlistPath); + startDevServerHelper(); } else if (device.type === 'device') { const { appPath, bundleIdentifier } = await buildApp({ args, @@ -169,12 +197,14 @@ ${devices deviceOrSimulator, fingerprintOptions, }); + await runOnDevice( device, appPath, projectConfig.sourceDir, bundleIdentifier, ); + startDevServerHelper(); } return; } else { @@ -223,6 +253,7 @@ ${devices fingerprintOptions, }), ]); + if (bootedDevice.type === 'simulator') { await runOnSimulator(bootedDevice, appPath, infoPlistPath); } else { @@ -234,6 +265,7 @@ ${devices ); } } + startDevServerHelper(); } }; diff --git a/packages/platform-apple-helpers/src/lib/commands/run/runOptions.ts b/packages/platform-apple-helpers/src/lib/commands/run/runOptions.ts index 1bacf0383..29e3affe2 100644 --- a/packages/platform-apple-helpers/src/lib/commands/run/runOptions.ts +++ b/packages/platform-apple-helpers/src/lib/commands/run/runOptions.ts @@ -8,6 +8,8 @@ export interface RunFlags extends BuildFlags { device?: string; catalyst?: boolean; local?: boolean; + devServer?: boolean; + clientLogs?: boolean; } export const getRunOptions = ({ platformName }: BuilderCommand) => { @@ -30,6 +32,14 @@ export const getRunOptions = ({ platformName }: BuilderCommand) => { name: '--catalyst', description: 'Run on Mac Catalyst.', }, + { + name: '--client-logs', + description: 'Enable client logs in dev server.', + }, + { + name: '--dev-server', + description: 'Automatically start a dev server (bundler) after building the app.', + }, ...getBuildOptions({ platformName }), ]; }; diff --git a/packages/platform-ios/src/lib/platformIOS.ts b/packages/platform-ios/src/lib/platformIOS.ts index 9703ba160..bae946659 100644 --- a/packages/platform-ios/src/lib/platformIOS.ts +++ b/packages/platform-ios/src/lib/platformIOS.ts @@ -61,6 +61,9 @@ export const platformIOS = remoteCacheProvider: await api.getRemoteCacheProvider(), fingerprintOptions: api.getFingerprintOptions(), reactNativePath: api.getReactNativePath(), + reactNativeVersion: api.getReactNativeVersion(), + platforms: api.getPlatforms(), + startDevServer: api.getBundlerStart(), }); outro('Success 🎉.'); }, diff --git a/packages/plugin-metro/src/index.ts b/packages/plugin-metro/src/index.ts index de3870480..aa80ef452 100644 --- a/packages/plugin-metro/src/index.ts +++ b/packages/plugin-metro/src/index.ts @@ -1 +1,2 @@ export * from './lib/pluginMetro.js'; +export { startDevServer } from './lib/start/command.js'; diff --git a/packages/plugin-metro/src/lib/pluginMetro.ts b/packages/plugin-metro/src/lib/pluginMetro.ts index 83aade990..2529d9339 100644 --- a/packages/plugin-metro/src/lib/pluginMetro.ts +++ b/packages/plugin-metro/src/lib/pluginMetro.ts @@ -1,16 +1,17 @@ -import type { PluginApi, PluginOutput } from '@rock-js/config'; +import type { BundlerPluginOutput, PluginApi } from '@rock-js/config'; import { registerBundleCommand } from './bundle/command.js'; -import { registerStartCommand } from './start/command.js'; +import { registerStartCommand, startDevServer } from './start/command.js'; export const pluginMetro = () => - (api: PluginApi): PluginOutput => { + (api: PluginApi): BundlerPluginOutput => { registerStartCommand(api); registerBundleCommand(api); return { name: '@rock-js/plugin-metro', description: 'Rock plugin for Metro bundler.', + start: startDevServer, }; }; diff --git a/packages/plugin-metro/src/lib/start/attachKeyHandlers.ts b/packages/plugin-metro/src/lib/start/attachKeyHandlers.ts index 0be0e2ef2..ad71a7c98 100644 --- a/packages/plugin-metro/src/lib/start/attachKeyHandlers.ts +++ b/packages/plugin-metro/src/lib/start/attachKeyHandlers.ts @@ -58,6 +58,8 @@ export default function attachKeyHandlers({ readline.emitKeypressEvents(process.stdin); setRawMode(true); + // resume stdin to allow reading keypresses + process.stdin.resume(); const reload = throttle(() => { reporter.update({ diff --git a/packages/plugin-metro/src/lib/start/command.ts b/packages/plugin-metro/src/lib/start/command.ts index 8f3cb33b2..6572a3174 100644 --- a/packages/plugin-metro/src/lib/start/command.ts +++ b/packages/plugin-metro/src/lib/start/command.ts @@ -7,37 +7,45 @@ import path from 'node:path'; import type { PluginApi } from '@rock-js/config'; +import type { StartDevServerArgs } from '@rock-js/config'; import { findDevServerPort, intro } from '@rock-js/tools'; import type { StartCommandArgs } from './runServer.js'; import runServer from './runServer.js'; +export async function startDevServer({ + root, + args, + reactNativeVersion, + reactNativePath, + platforms, +}: StartDevServerArgs) { + const { port, startDevServer } = await findDevServerPort( + args.port ? Number(args.port) : 8081, + root, + ); + + if (!startDevServer) { + return; + } + + return runServer( + { root, reactNativeVersion, reactNativePath, platforms }, + { ...args, port, platforms: Object.keys(platforms) }, + ); +} + export const registerStartCommand = (api: PluginApi) => { api.registerCommand({ name: 'start', action: async (args: StartCommandArgs) => { intro('Starting Metro dev server'); - const root = api.getProjectRoot(); - const { port, startDevServer } = await findDevServerPort( - args.port ? Number(args.port) : 8081, - root, - ); - - if (!startDevServer) { - return; - } - - const reactNativeVersion = api.getReactNativeVersion(); - const reactNativePath = api.getReactNativePath(); - const platforms = api.getPlatforms(); - return runServer( - { - root, - reactNativeVersion, - reactNativePath, - platforms, - }, - { ...args, port }, - ); + startDevServer({ + root: api.getProjectRoot(), + reactNativeVersion: api.getReactNativeVersion(), + reactNativePath: api.getReactNativePath(), + platforms: api.getPlatforms(), + args, + }); }, description: 'Start the Metro development server.', options: [ diff --git a/packages/plugin-repack/src/index.ts b/packages/plugin-repack/src/index.ts index 8530c0676..10556cbda 100644 --- a/packages/plugin-repack/src/index.ts +++ b/packages/plugin-repack/src/index.ts @@ -1 +1,2 @@ export * from './lib/pluginRepack.js'; +export { startDevServer } from './lib/pluginRepack.js'; diff --git a/packages/plugin-repack/src/lib/pluginRepack.ts b/packages/plugin-repack/src/lib/pluginRepack.ts index 659414c01..16bee6eae 100644 --- a/packages/plugin-repack/src/lib/pluginRepack.ts +++ b/packages/plugin-repack/src/lib/pluginRepack.ts @@ -1,5 +1,9 @@ import commands from '@callstack/repack/commands/rspack'; -import type { PluginApi, PluginOutput } from '@rock-js/config'; +import type { + BundlerPluginOutput, + PluginApi, + StartDevServerArgs, +} from '@rock-js/config'; import { colorLink, findDevServerPort, @@ -25,9 +29,42 @@ type BundleArgs = Parameters['func']>[2] & { const startCommand = commands.find((command) => command.name === 'start'); const bundleCommand = commands.find((command) => command.name === 'bundle'); +export async function startDevServer( + { + root, + args, + reactNativeVersion: _reactNativeVersion, + reactNativePath, + platforms, + }: StartDevServerArgs, + pluginConfig: PluginConfig = {}, +) { + const { port, startDevServer } = await findDevServerPort( + args.port ? Number(args.port) : 8081, + root, + ); + + if (!startDevServer) { + return; + } + + if (!startCommand) { + throw new RockError('Re.Pack "start" command not found.'); + } + + logger.info('Starting Re.Pack dev server...'); + + startCommand.func( + [], + // @ts-expect-error TODO fix getPlatforms type + { reactNativePath, root, platforms, ...pluginConfig }, + { ...args, port }, + ); +} + export const pluginRepack = (pluginConfig: PluginConfig = {}) => - (api: PluginApi): PluginOutput => { + (api: PluginApi): BundlerPluginOutput => { if (!startCommand) { throw new RockError('Re.Pack "start" command not found.'); } @@ -40,24 +77,14 @@ export const pluginRepack = name: 'start', description: 'Starts Re.Pack dev server.', action: async (args: StartArgs) => { - const reactNativePath = api.getReactNativePath(); - const root = api.getProjectRoot(); - const platforms = api.getPlatforms(); - const { port, startDevServer } = await findDevServerPort( - args.port ? Number(args.port) : 8081, - root, - ); - - if (!startDevServer) { - return; - } - - startCommand.func( - [], - // @ts-expect-error TODO fix getPlatforms type - { reactNativePath, root, platforms, ...pluginConfig }, - { ...args, port }, - ); + startDevServer({ + root: api.getProjectRoot(), + reactNativeVersion: api.getReactNativeVersion(), + reactNativePath: api.getReactNativePath(), + platforms: api.getPlatforms(), + // @ts-expect-error Re.Pack doesn't have clientLogs + args, + }); }, // @ts-expect-error fixup types options: startCommand.options, @@ -117,6 +144,7 @@ export const pluginRepack = return { name: '@rock-js/plugin-repack', description: 'Rock plugin for Re.Pack toolkit with Rspack.', + start: startDevServer, }; }; diff --git a/website/src/docs/cli/introduction.md b/website/src/docs/cli/introduction.md index a5f840d68..6b80c3cfa 100644 --- a/website/src/docs/cli/introduction.md +++ b/website/src/docs/cli/introduction.md @@ -182,13 +182,14 @@ The build cache is populated either by a local build or when downloaded frome re `run:ios` extends the functionality of `build:ios` with additional runtime options. -| Option | Description | -| :----------------------- | :---------------------------------------- | -| `--port ` | Bundler port (default: 8081) | -| `--binary-path ` | Path to pre-built .app binary | -| `--device ` | Device/simulator to use (by name or UDID) | -| `--catalyst` | Run on Mac Catalyst | -| `--local` | Force local build with xcodebuild | +| Option | Description | +| :----------------------- | :----------------------------------------------------------------- | +| `--port ` | Bundler port (default: 8081) | +| `--binary-path ` | Path to pre-built .app binary | +| `--device ` | Device/simulator to use (by name or UDID) | +| `--catalyst` | Run on Mac Catalyst | +| `--local` | Force local build with xcodebuild | +| `--dev-server` | Automatically start a dev server (bundler) after building the app. | You can also pass the same environmental variables listed in [`build:ios` options](#supported-environmental-variables) to the `run:ios` command. @@ -235,12 +236,13 @@ The `run:android` command runs your Android app on an emulator or device. It ext Same as for `build:android` and: -| Option | Description | -| :------------------------- | :------------------------------------ | -| `--app-id ` | Application ID | -| `--app-id-suffix ` | Application ID suffix | -| `--binary-path ` | Path to pre-built APK | -| `--local` | Force local build with Gradle wrapper | +| Option | Description | +| :------------------------- | :----------------------------------------------------------------- | +| `--app-id ` | Application ID | +| `--app-id-suffix ` | Application ID suffix | +| `--binary-path ` | Path to pre-built APK | +| `--local` | Force local build with Gradle wrapper | +| `--dev-server` | Automatically start a dev server (bundler) after building the app. | ### `rock sign:android` Options