mirror of
https://github.com/moeru-ai/airi.git
synced 2026-08-14 08:52:42 +00:00
feat(cap-vite): auto select the first device when no target specified
This commit is contained in:
@@ -20,6 +20,7 @@ pnpm -F @proj-airi/stage-pocket run dev:ios -- --target <DEVICE_ID_OR_SIMULATOR_
|
||||
- Arguments before `--` are forwarded to `vite`.
|
||||
- Arguments after `--` are forwarded to `cap run`.
|
||||
- If the platform-specific env is set (`CAPACITOR_DEVICE_ID_IOS` or `CAPACITOR_DEVICE_ID_ANDROID`) and `cap run` args do not contain `--target`, `cap-vite` injects `--target` with that value automatically.
|
||||
- If no `--target` argument or platform-specific env is set, `cap-vite` uses the first target from `cap run <platform> --list --json`.
|
||||
- `cap-vite` always launches the Vite dev server. Do not pass `vite dev` or `vite serve` as extra args.
|
||||
- After the dev server starts, press `R` in the terminal to re-run `cap run` without restarting Vite.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { hasCapacitorTargetArg, parseCapacitorPlatform, pickServerUrl, resolveCapRunArgs, shouldRestartForNativeChange } from './native'
|
||||
|
||||
@@ -43,40 +43,82 @@ describe('pickServerUrl', () => {
|
||||
})
|
||||
|
||||
describe('resolveCapRunArgs', () => {
|
||||
it('keeps an explicit --target argument untouched', () => {
|
||||
expect(resolveCapRunArgs(
|
||||
it('keeps an explicit --target argument untouched', async () => {
|
||||
await expect(resolveCapRunArgs(
|
||||
['ios', '--target', 'iPhone 16 Pro', '--scheme', 'AIRI'],
|
||||
{ CAPACITOR_DEVICE_ID_IOS: 'ignored-device' },
|
||||
)).toEqual(['ios', '--target', 'iPhone 16 Pro', '--scheme', 'AIRI'])
|
||||
)).resolves.toEqual(['ios', '--target', 'iPhone 16 Pro', '--scheme', 'AIRI'])
|
||||
})
|
||||
|
||||
it('injects --target from CAPACITOR_DEVICE_ID_ANDROID when it is missing', () => {
|
||||
expect(resolveCapRunArgs(
|
||||
it('injects --target from CAPACITOR_DEVICE_ID_ANDROID when it is missing', async () => {
|
||||
await expect(resolveCapRunArgs(
|
||||
['android', '--flavor', 'release'],
|
||||
{ CAPACITOR_DEVICE_ID_ANDROID: 'emulator-5554' },
|
||||
)).toEqual(['android', '--target', 'emulator-5554', '--flavor', 'release'])
|
||||
)).resolves.toEqual(['android', '--target', 'emulator-5554', '--flavor', 'release'])
|
||||
})
|
||||
|
||||
it('injects --target from CAPACITOR_DEVICE_ID_IOS when it is missing', () => {
|
||||
expect(resolveCapRunArgs(
|
||||
it('injects --target from CAPACITOR_DEVICE_ID_IOS when it is missing', async () => {
|
||||
await expect(resolveCapRunArgs(
|
||||
['ios', '--scheme', 'AIRI'],
|
||||
{ CAPACITOR_DEVICE_ID_IOS: 'iPhone 16 Pro' },
|
||||
)).toEqual(['ios', '--target', 'iPhone 16 Pro', '--scheme', 'AIRI'])
|
||||
)).resolves.toEqual(['ios', '--target', 'iPhone 16 Pro', '--scheme', 'AIRI'])
|
||||
})
|
||||
|
||||
it('does not use the other platform device id', () => {
|
||||
expect(resolveCapRunArgs(
|
||||
it('does not use the other platform device id', async () => {
|
||||
const listTargets = vi.fn(async () => [
|
||||
{ id: 'ios-device' },
|
||||
])
|
||||
|
||||
await expect(resolveCapRunArgs(
|
||||
['ios'],
|
||||
{ CAPACITOR_DEVICE_ID_ANDROID: 'emulator-5554' },
|
||||
)).toEqual(['ios'])
|
||||
listTargets,
|
||||
)).resolves.toEqual(['ios', '--target', 'ios-device'])
|
||||
})
|
||||
|
||||
it('supports the --target=value form when checking existing args', () => {
|
||||
it('supports the --target=value form when checking existing args', async () => {
|
||||
expect(hasCapacitorTargetArg(['android', '--target=emulator-5554'])).toBe(true)
|
||||
expect(resolveCapRunArgs(
|
||||
await expect(resolveCapRunArgs(
|
||||
['android', '--target=emulator-5554', '--flavor', 'release'],
|
||||
{ CAPACITOR_DEVICE_ID_ANDROID: 'ignored-device' },
|
||||
)).toEqual(['android', '--target=emulator-5554', '--flavor', 'release'])
|
||||
)).resolves.toEqual(['android', '--target=emulator-5554', '--flavor', 'release'])
|
||||
})
|
||||
|
||||
it('injects the first listed device when --target and platform device env are missing', async () => {
|
||||
const listTargets = vi.fn(async () => [
|
||||
{ id: 'first-device' },
|
||||
{ id: 'second-device' },
|
||||
])
|
||||
|
||||
await expect(resolveCapRunArgs(
|
||||
['android', '--flavor', 'release'],
|
||||
{},
|
||||
listTargets,
|
||||
)).resolves.toEqual(['android', '--target', 'first-device', '--flavor', 'release'])
|
||||
expect(listTargets).toHaveBeenCalledWith('android')
|
||||
})
|
||||
|
||||
it('prefers platform device env over the first listed device', async () => {
|
||||
const listTargets = vi.fn(async () => [
|
||||
{ id: 'first-device' },
|
||||
])
|
||||
|
||||
await expect(resolveCapRunArgs(
|
||||
['ios'],
|
||||
{ CAPACITOR_DEVICE_ID_IOS: 'configured-device' },
|
||||
listTargets,
|
||||
)).resolves.toEqual(['ios', '--target', 'configured-device'])
|
||||
expect(listTargets).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('throws when no default device target is available', async () => {
|
||||
const listTargets = vi.fn(async () => [])
|
||||
|
||||
await expect(resolveCapRunArgs(
|
||||
['ios'],
|
||||
{},
|
||||
listTargets,
|
||||
)).rejects.toThrow('No ios devices or simulators found.')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -4,8 +4,16 @@ import process from 'node:process'
|
||||
|
||||
import { basename, extname, relative, resolve, sep } from 'node:path'
|
||||
|
||||
import { x } from 'tinyexec'
|
||||
|
||||
export type CapacitorPlatform = 'android' | 'ios'
|
||||
|
||||
interface CapacitorTarget {
|
||||
id?: string
|
||||
}
|
||||
|
||||
type ListCapacitorTargets = (platform: CapacitorPlatform) => Promise<readonly CapacitorTarget[]>
|
||||
|
||||
const nativeExtensionsByPlatform: Record<CapacitorPlatform, Set<string>> = {
|
||||
ios: new Set([
|
||||
'.entitlements',
|
||||
@@ -84,7 +92,42 @@ export function hasCapacitorTargetArg(capArgs: string[]): boolean {
|
||||
return capArgs.some((arg, index) => arg === '--target' || (index > 0 && arg.startsWith('--target=')))
|
||||
}
|
||||
|
||||
export function resolveCapRunArgs(capArgs: string[], env: NodeJS.ProcessEnv = process.env): string[] {
|
||||
function parseCapacitorTargetList(value: string): CapacitorTarget[] {
|
||||
const parsed = JSON.parse(value)
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new TypeError('Expected `cap run --list --json` to return a JSON array.')
|
||||
}
|
||||
|
||||
return parsed
|
||||
.filter((target): target is CapacitorTarget => typeof target === 'object' && target !== null && typeof (target as CapacitorTarget).id === 'string')
|
||||
}
|
||||
|
||||
async function listCapacitorTargets(platform: CapacitorPlatform): Promise<CapacitorTarget[]> {
|
||||
const output = await x('cap', ['run', platform, '--list', '--json'])
|
||||
|
||||
return parseCapacitorTargetList(output.stdout)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves Capacitor run arguments by applying target defaults.
|
||||
*
|
||||
* Use when:
|
||||
* - `cap-vite` is about to run `cap run`
|
||||
* - Callers want env-based device IDs before falling back to the first available target
|
||||
*
|
||||
* Expects:
|
||||
* - `capArgs[0]` is already validated as `ios` or `android` by the CLI boundary
|
||||
* - Explicit `--target` arguments must stay untouched so Capacitor can validate them
|
||||
*
|
||||
* Returns:
|
||||
* - The original args when a target is explicit
|
||||
* - Args with `--target` injected from env or the first listed Capacitor target
|
||||
*/
|
||||
export async function resolveCapRunArgs(
|
||||
capArgs: string[],
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
listTargets: ListCapacitorTargets = listCapacitorTargets,
|
||||
): Promise<string[]> {
|
||||
if (capArgs.length === 0 || hasCapacitorTargetArg(capArgs)) {
|
||||
return capArgs
|
||||
}
|
||||
@@ -100,7 +143,16 @@ export function resolveCapRunArgs(capArgs: string[], env: NodeJS.ProcessEnv = pr
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
return capArgs
|
||||
if (!platform) {
|
||||
return capArgs
|
||||
}
|
||||
|
||||
const targets = await listTargets(platform)
|
||||
target = targets.find(device => device.id)?.id
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
throw new Error(`No ${platform} devices or simulators found. Connect a device, start a simulator or emulator, or pass --target explicitly.`)
|
||||
}
|
||||
|
||||
return [platformArg, '--target', target, ...rest]
|
||||
|
||||
@@ -82,7 +82,7 @@ function createMockStdin() {
|
||||
return stdin
|
||||
}
|
||||
|
||||
function configurePluginServer(plugin: Plugin, server: ReturnType<typeof createMockServer>) {
|
||||
async function configurePluginServer(plugin: Plugin, server: ReturnType<typeof createMockServer>) {
|
||||
const configureServer = plugin.configureServer
|
||||
if (!configureServer) {
|
||||
throw new Error('cap-vite plugin is missing configureServer().')
|
||||
@@ -92,7 +92,7 @@ function configurePluginServer(plugin: Plugin, server: ReturnType<typeof createM
|
||||
? configureServer
|
||||
: configureServer.handler
|
||||
|
||||
handler.call({} as any, server as any)
|
||||
await handler.call({} as any, server as any)
|
||||
}
|
||||
|
||||
const originalStdin = Object.getOwnPropertyDescriptor(process, 'stdin')
|
||||
@@ -123,15 +123,15 @@ describe('capVitePlugin', () => {
|
||||
const { capVitePlugin } = await import('./vite-plugin')
|
||||
const server = createMockServer()
|
||||
|
||||
configurePluginServer(capVitePlugin({
|
||||
capArgs: ['ios', '--scheme', 'AIRI'],
|
||||
await configurePluginServer(capVitePlugin({
|
||||
capArgs: ['ios', '--target', 'iPhone 16 Pro', '--scheme', 'AIRI'],
|
||||
}), server)
|
||||
|
||||
server.httpServer.emit('listening')
|
||||
|
||||
expect(emitKeypressEvents).toHaveBeenCalledWith(stdin)
|
||||
expect(stdin.setRawMode).toHaveBeenCalledWith(true)
|
||||
expect(x).toHaveBeenNthCalledWith(1, 'cap', ['run', 'ios', '--scheme', 'AIRI'], {
|
||||
expect(x).toHaveBeenNthCalledWith(1, 'cap', ['run', 'ios', '--target', 'iPhone 16 Pro', '--scheme', 'AIRI'], {
|
||||
nodeOptions: {
|
||||
cwd: '/repo/app',
|
||||
env: {
|
||||
@@ -146,8 +146,8 @@ describe('capVitePlugin', () => {
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(firstRun.kill).toHaveBeenCalledWith('SIGINT')
|
||||
expect(server.config.logger.info).toHaveBeenCalledWith('[cap-vite] manual restart requested. Re-running "cap run ios --scheme AIRI".')
|
||||
expect(x).toHaveBeenNthCalledWith(2, 'cap', ['run', 'ios', '--scheme', 'AIRI'], {
|
||||
expect(server.config.logger.info).toHaveBeenCalledWith('[cap-vite] manual restart requested. Re-running "cap run ios --target iPhone 16 Pro --scheme AIRI".')
|
||||
expect(x).toHaveBeenNthCalledWith(2, 'cap', ['run', 'ios', '--target', 'iPhone 16 Pro', '--scheme', 'AIRI'], {
|
||||
nodeOptions: {
|
||||
cwd: '/repo/app',
|
||||
env: {
|
||||
@@ -180,8 +180,8 @@ describe('capVitePlugin', () => {
|
||||
const { capVitePlugin } = await import('./vite-plugin')
|
||||
const server = createMockServer()
|
||||
|
||||
configurePluginServer(capVitePlugin({
|
||||
capArgs: ['android'],
|
||||
await configurePluginServer(capVitePlugin({
|
||||
capArgs: ['android', '--target', 'emulator-5554'],
|
||||
}), server)
|
||||
|
||||
server.httpServer.emit('listening')
|
||||
|
||||
@@ -102,8 +102,7 @@ function bindCapViteShortcuts(
|
||||
}
|
||||
|
||||
export function capVitePlugin(options: CapVitePluginOptions): Plugin {
|
||||
const resolvedCapArgs = resolveCapRunArgs(options.capArgs)
|
||||
const platform = parseCapacitorPlatform(resolvedCapArgs[0])
|
||||
const platform = parseCapacitorPlatform(options.capArgs[0])
|
||||
if (!platform) {
|
||||
throw new Error('The first `cap run` argument must be `ios` or `android`.')
|
||||
}
|
||||
@@ -112,7 +111,8 @@ export function capVitePlugin(options: CapVitePluginOptions): Plugin {
|
||||
return {
|
||||
apply: 'serve',
|
||||
name: 'cap-vite:run-capacitor',
|
||||
configureServer(server) {
|
||||
async configureServer(server) {
|
||||
const resolvedCapArgs = await resolveCapRunArgs(options.capArgs)
|
||||
const cwd = resolve(server.config.root)
|
||||
const platformRoot = resolve(cwd, resolvedPlatform)
|
||||
const debounceMs = 300
|
||||
|
||||
Reference in New Issue
Block a user