refactor(cap-vite): forward options to capacitor cli
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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...>]')
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
})
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
})
|
||||
Generated
+3
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user