refactor(cap-vite): forward options to capacitor cli

This commit is contained in:
LemonNeko
2026-03-10 18:39:52 +08:00
parent 8cfdfe2468
commit cfd897ce2a
10 changed files with 221 additions and 17 deletions
+6 -2
View File
@@ -5,13 +5,17 @@ CLI for [Capacitor](https://capacitorjs.com/) live-reload development using Vite
## Usage
```bash
pnpm cap-vite ios <DEVICE_ID_OR_SIMULATOR_NAME>
pnpm cap-vite android <DEVICE_ID_OR_SIMULATOR_NAME>
pnpm cap-vite ios --target <DEVICE_ID_OR_SIMULATOR_NAME>
pnpm cap-vite android --target <DEVICE_ID_OR_SIMULATOR_NAME>
pnpm cap-vite ios --target <DEVICE_ID_OR_SIMULATOR_NAME> -- --scheme AIRI
pnpm cap-vite android --target <DEVICE_ID_OR_SIMULATOR_NAME> -- --flavor release
# Or
CAPACITOR_DEVICE_ID=<DEVICE_ID_OR_SIMULATOR_NAME> pnpm cap-vite ios
CAPACITOR_DEVICE_ID=<DEVICE_ID_OR_SIMULATOR_NAME> pnpm cap-vite android
```
- Arguments after `--` are forwarded to `cap run`, example: `pnpm cap-vite ios --target <DEVICE_ID_OR_SIMULATOR_NAME> -- --scheme AIRI` will run `cap run ios --target <DEVICE_ID_OR_SIMULATOR_NAME> --scheme AIRI`.
You can see the list of available devices and simulators by running `pnpm exec cap run ios --list` or `pnpm exec cap run android --list`.
## Capacitor Configuration
+2
View File
@@ -32,6 +32,7 @@
],
"scripts": {
"build": "tsdown",
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
@@ -39,6 +40,7 @@
"vite": "^7.0.0 || ^8.0.0-beta.0"
},
"dependencies": {
"cac": "^6.7.14",
"tinyexec": "^1.0.2"
}
}
+5 -10
View File
@@ -3,20 +3,15 @@
import process from 'node:process'
import { runCapVite } from '..'
import { parseCapViteCliArgs } from '../cli'
async function main() {
const platform = process.argv[2]
const deviceId = process.env.CAPACITOR_DEVICE_ID || process.argv[3]
if (!deviceId) {
throw new Error('Usage: cap-vite <ios|android> <DEVICE_ID_OR_SIMULATOR_NAME>')
const parsed = parseCapViteCliArgs(process.argv.slice(2))
if (!parsed) {
return
}
if (platform !== 'android' && platform !== 'ios') {
process.stderr.write('Usage: cap-vite <ios|android> <DEVICE_ID_OR_SIMULATOR_NAME>\n')
process.exit(1)
}
await runCapVite(platform, deviceId)
await runCapVite(parsed.platform, parsed.target, { capArgs: parsed.capArgs })
}
void main().catch((error) => {
+41
View File
@@ -0,0 +1,41 @@
import { describe, expect, it } from 'vitest'
import { parseCapViteCliArgs } from './cli'
describe('parseCapViteCliArgs', () => {
it('parses target and cap args after double dashes', () => {
expect(parseCapViteCliArgs(['ios', '--target', 'iPhone 16 Pro', '--', '--flavor', 'release'])).toEqual({
capArgs: ['--flavor', 'release'],
platform: 'ios',
target: 'iPhone 16 Pro',
})
})
it('falls back to CAPACITOR_DEVICE_ID when --target is omitted', () => {
expect(parseCapViteCliArgs(['android'], { CAPACITOR_DEVICE_ID: 'emulator-5554' })).toEqual({
capArgs: [],
platform: 'android',
target: 'emulator-5554',
})
})
it('keeps cap args untouched even when they look like wrapper flags', () => {
expect(parseCapViteCliArgs(['android', '--target', 'emulator-5554', '--', '--mode', 'release'])).toEqual({
capArgs: ['--mode', 'release'],
platform: 'android',
target: 'emulator-5554',
})
})
it('rejects unknown wrapper options before double dashes', () => {
expect(() => parseCapViteCliArgs(['ios', '--target', 'iPhone 16 Pro', '--mode', 'mobile'])).toThrow(/Unknown option `--mode`/)
})
it('allows only one positional argument', () => {
expect(() => parseCapViteCliArgs(['ios', 'iPhone 16 Pro'])).toThrow('cap-vite <ios|android> [--target <DEVICE_ID_OR_SIMULATOR_NAME>] [-- <cap args...>]')
})
it('rejects unsupported platforms', () => {
expect(() => parseCapViteCliArgs(['web', '--target', 'chrome'])).toThrow('cap-vite <ios|android> [--target <DEVICE_ID_OR_SIMULATOR_NAME>] [-- <cap args...>]')
})
})
+63
View File
@@ -0,0 +1,63 @@
import type { CapacitorPlatform } from '.'
import process from 'node:process'
import { cac } from 'cac'
export interface ParsedCapViteCliArgs {
capArgs: string[]
platform: CapacitorPlatform
target: string
}
const usage = 'cap-vite <ios|android> [--target <DEVICE_ID_OR_SIMULATOR_NAME>] [-- <cap args...>]'
function createCapViteCli() {
const cli = cac('cap-vite')
cli.help()
cli
.command('<platform>', 'Run Capacitor with a Vite dev server')
.usage(usage)
.option('--target <target>', 'Set the Capacitor device target')
.example('cap-vite ios --target "iPhone 16 Pro" -- --scheme AIRI')
.example('CAPACITOR_DEVICE_ID=emulator-5554 cap-vite android -- --flavor release')
return cli
}
export function parseCapViteCliArgs(
argv: string[],
env: NodeJS.ProcessEnv = process.env,
): ParsedCapViteCliArgs | null {
const cli = createCapViteCli()
const parsed = cli.parse(['node', 'cap-vite', ...argv], { run: false })
if (cli.options.help) {
return null
}
cli.matchedCommand?.checkUnknownOptions()
cli.matchedCommand?.checkOptionValue()
cli.matchedCommand?.checkRequiredArgs()
if (parsed.args.length > 1) {
throw new Error(usage)
}
const platform = parsed.args[0]
if (platform !== 'android' && platform !== 'ios') {
throw new Error(usage)
}
const target = typeof parsed.options.target === 'string' ? parsed.options.target : env.CAPACITOR_DEVICE_ID
if (!target) {
throw new Error(usage)
}
return {
capArgs: Array.isArray(parsed.options['--']) ? parsed.options['--'] : [],
platform,
target,
}
}
+78
View File
@@ -0,0 +1,78 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { runCapVite } from './index'
const { createServer, x } = vi.hoisted(() => ({
createServer: vi.fn(),
x: vi.fn(),
}))
vi.mock('tinyexec', () => ({
x,
}))
vi.mock('vite', () => ({
createServer,
}))
describe('runCapVite', () => {
beforeEach(() => {
vi.clearAllMocks()
})
afterEach(() => {
vi.restoreAllMocks()
})
it('forwards cap args into cap run', async () => {
const processOnce = vi.spyOn(process, 'once').mockImplementation(() => process)
createServer.mockResolvedValue({
close: vi.fn().mockResolvedValue(undefined),
config: {
logger: {
info: vi.fn(),
},
},
listen: vi.fn().mockResolvedValue(undefined),
printUrls: vi.fn(),
resolvedUrls: {
local: ['http://127.0.0.1:5173/'],
},
watcher: {
add: vi.fn(),
off: vi.fn(),
on: vi.fn(),
unwatch: vi.fn().mockResolvedValue(undefined),
},
})
x.mockReturnValue({
kill: vi.fn(),
})
await runCapVite('android', 'emulator-5554', {
capArgs: ['--flavor', 'release'],
})
expect(createServer).toHaveBeenCalledWith({
clearScreen: false,
root: process.cwd(),
})
expect(x).toHaveBeenCalledWith('cap', ['run', 'android', '--target', 'emulator-5554', '--flavor', 'release'], {
nodeOptions: {
cwd: process.cwd(),
env: {
CAPACITOR_DEV_SERVER_URL: 'http://127.0.0.1:5173/',
},
stdio: 'inherit',
},
persist: true,
throwOnError: false,
})
expect(processOnce).toHaveBeenCalledWith('SIGINT', expect.any(Function))
expect(processOnce).toHaveBeenCalledWith('SIGTERM', expect.any(Function))
})
})
+9 -5
View File
@@ -11,6 +11,7 @@ import { createServer } from 'vite'
export type CapacitorPlatform = 'android' | 'ios'
export interface RunCapViteOptions {
capArgs?: string[]
cwd?: string
debounceMs?: number
}
@@ -142,15 +143,18 @@ async function stopCapProcess(current: Result | undefined) {
}
}
function startCapProcess(cwd: string, platform: CapacitorPlatform, deviceId: string, url: URL) {
return x('cap', ['run', platform, '--target', deviceId], { persist: true, throwOnError: false, nodeOptions: { cwd, stdio: 'inherit', env: { CAPACITOR_DEV_SERVER_URL: url.toString() } } })
function startCapProcess(cwd: string, platform: CapacitorPlatform, target: string, url: URL, capArgs: string[]) {
console.info('\n----------------------\n')
console.info('Running cap run', platform, '--target', target, ...capArgs)
return x('cap', ['run', platform, '--target', target, ...capArgs], { persist: true, throwOnError: false, nodeOptions: { cwd, stdio: 'inherit', env: { CAPACITOR_DEV_SERVER_URL: url.toString() } } })
}
export async function runCapVite(
platform: CapacitorPlatform,
deviceId: string,
target: string,
options: RunCapViteOptions = {},
): Promise<void> {
const capArgs = options.capArgs ?? []
const cwd = resolve(options.cwd ?? process.cwd())
const debounceMs = options.debounceMs ?? 300
const server = await createServer({
@@ -164,7 +168,7 @@ export async function runCapVite(
const url = pickServerUrl(server)
const logger = server.config.logger
let currentCapProcess: Result | undefined = startCapProcess(cwd, platform, deviceId, url)
let currentCapProcess: Result | undefined = startCapProcess(cwd, platform, target, url, capArgs)
let restartTimer: NodeJS.Timeout | undefined
let shuttingDown = false
@@ -177,7 +181,7 @@ export async function runCapVite(
const previous = currentCapProcess
currentCapProcess = undefined
await stopCapProcess(previous)
currentCapProcess = startCapProcess(cwd, platform, deviceId, url)
currentCapProcess = startCapProcess(cwd, platform, target, url, capArgs)
}
const onWatcherEvent = (_event: string, file: string) => {
+13
View File
@@ -0,0 +1,13 @@
import { dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vitest/config'
const root = dirname(fileURLToPath(import.meta.url))
export default defineConfig({
root,
test: {
include: ['src/**/*.test.ts'],
},
})
+3
View File
@@ -1963,6 +1963,9 @@ importers:
'@capacitor/cli':
specifier: ^8.0.0
version: 8.1.0
cac:
specifier: ^6.7.14
version: 6.7.14
tinyexec:
specifier: ^1.0.2
version: 1.0.2
+1
View File
@@ -7,6 +7,7 @@ export default defineConfig({
'apps/stage-tamagotchi',
'packages/stage-ui',
'packages/plugin-sdk',
'packages/cap-vite',
'packages/vite-plugin-warpdrive',
'packages/audio-pipelines-transcribe',
'packages/server-runtime',