feat(stage-pocket): introducing a new package for better DX
This commit is contained in:
@@ -175,31 +175,15 @@ nix run github:moeru-ai/airi
|
||||
|
||||
### Stage Pocket (Mobile Version)
|
||||
|
||||
Start the development server for the capacitor web version:
|
||||
Start the development server for the capacitor:
|
||||
|
||||
```shell
|
||||
pnpm dev:pocket
|
||||
pnpm dev:pocket:ios <DEVICE_ID_OR_SIMULATOR_NAME>
|
||||
# Or
|
||||
CAPACITOR_DEVICE_ID=<DEVICE_ID_OR_SIMULATOR_NAME> pnpm dev:pocket:ios
|
||||
```
|
||||
|
||||
Check your IP address in the output of the command above:
|
||||
|
||||
```shell
|
||||
ROLLDOWN-VITE v7.3.0 ready in 1073 ms
|
||||
|
||||
➜ Local: https://localhost:5273/
|
||||
➜ Network: https://<ip-will-be-here>:5273/
|
||||
➜ Vue DevTools: Open https://localhost:5273/__devtools__/ as a separate window
|
||||
➜ Vue DevTools: Press Option(⌥)+Shift(⇧)+D in App to toggle the Vue DevTools
|
||||
➜ UnoCSS Inspector: https://localhost:5273/__unocss/
|
||||
```
|
||||
|
||||
Open the Xcode project:
|
||||
|
||||
```shell
|
||||
CAPACITOR_DEV_SERVER_URL=https://<your-ip-address>:5273 pnpm open:ios
|
||||
```
|
||||
|
||||
Then Xcode will open and you can click the "Run" button to run the app on your iPhone.
|
||||
You can see the list of available devices and simulators by running `pnpm exec cap run ios --list`.
|
||||
|
||||
If you need to connect server channel on pocket in wireless mode, you need to start tamagotchi as root:
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { env } from 'node:process'
|
||||
const serverURL = env.CAPACITOR_DEV_SERVER_URL
|
||||
|
||||
const config: CapacitorConfig = {
|
||||
appId: 'ai.moeru.airi',
|
||||
appId: 'ai.moeru.airi-pocket',
|
||||
appName: 'AIRI',
|
||||
webDir: 'dist',
|
||||
server: serverURL
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
"preview": "vite preview",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"dev:web": "vite",
|
||||
"dev:ios": "VITE_CAP_SYNC_IOS_AFTER_BUILD=1 vite build --watch",
|
||||
"open:ios": "vite build && cap sync ios && cap open ios"
|
||||
"dev:ios": "cap-vite ios"
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor/core": "catalog:",
|
||||
@@ -111,6 +110,7 @@
|
||||
"@iconify-json/svg-spinners": "^1.2.4",
|
||||
"@iconify-json/vscode-icons": "^1.2.44",
|
||||
"@intlify/unplugin-vue-i18n": "^11.0.7",
|
||||
"@proj-airi/cap-vite": "workspace:*",
|
||||
"@proj-airi/iconify-meteocons": "catalog:",
|
||||
"@proj-airi/lobe-icons": "^1.0.18",
|
||||
"@proj-airi/unplugin-fetch": "catalog:",
|
||||
|
||||
+2
-1
@@ -17,7 +17,8 @@
|
||||
"dev:docs": "pnpm -rF @proj-airi/docs run dev",
|
||||
"dev:ui": "pnpm -rF @proj-airi/stage-ui run story:dev",
|
||||
"dev:web": "pnpm -rF @proj-airi/stage-web run dev",
|
||||
"dev:pocket": "pnpm -rF @proj-airi/stage-pocket run dev:web",
|
||||
"dev:pocket:ios": "pnpm -rF @proj-airi/stage-pocket run dev:ios",
|
||||
"dev:pocket:android": "pnpm -rF @proj-airi/stage-pocket run dev:android",
|
||||
"dev:server": "pnpm -rF @proj-airi/server-runtime run dev",
|
||||
"dev:tamagotchi": "pnpm -rF @proj-airi/stage-tamagotchi run dev",
|
||||
"dev:apps": "pnpm -rF=\"./apps/*\" run --parallel dev",
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# @proj-airi/cap-vite
|
||||
|
||||
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>
|
||||
# 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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
You need to set `server.url` in `capacitor.config.ts` to the env variable `CAPACITOR_DEV_SERVER_URL`.
|
||||
|
||||
```ts
|
||||
const serverURL = env.CAPACITOR_DEV_SERVER_URL
|
||||
|
||||
const config: CapacitorConfig = {
|
||||
appId: 'com.example.app',
|
||||
appName: 'Example App',
|
||||
webDir: 'dist',
|
||||
server: serverURL
|
||||
? {
|
||||
url: serverURL,
|
||||
cleartext: false,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
|
||||
export default config
|
||||
```
|
||||
|
||||
## What It Does
|
||||
|
||||
- Starts the project's own Vite config through the Vite API.
|
||||
- Executes the local `cap` binary via `tinyexec`.
|
||||
- Watches native files under `ios/` or `android/` and re-runs `cap run` after a small debounce.
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@proj-airi/cap-vite",
|
||||
"type": "module",
|
||||
"version": "0.9.0-alpha.2",
|
||||
"private": true,
|
||||
"description": "CLI that starts a Vite dev server and runs Capacitor live reload",
|
||||
"author": {
|
||||
"name": "Moeru AI Project AIRI Team",
|
||||
"email": "airi@moeru.ai",
|
||||
"url": "https://github.com/moeru-ai"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/moeru-ai/airi.git",
|
||||
"directory": "packages/cap-vite"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"default": "./dist/index.mjs"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.mts",
|
||||
"bin": {
|
||||
"cap-vite": "./dist/bin/run.mjs"
|
||||
},
|
||||
"files": [
|
||||
"README.md",
|
||||
"dist",
|
||||
"package.json"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@capacitor/cli": "^8.0.0",
|
||||
"vite": "^7.0.0 || ^8.0.0-beta.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tinyexec": "^1.0.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import process from 'node:process'
|
||||
|
||||
import { runCapVite } from '..'
|
||||
|
||||
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>')
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
void main().catch((error) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,219 @@
|
||||
import type { Result } from 'tinyexec'
|
||||
import type { ViteDevServer } from 'vite'
|
||||
|
||||
import process from 'node:process'
|
||||
|
||||
import { basename, extname, relative, resolve, sep } from 'node:path'
|
||||
|
||||
import { x } from 'tinyexec'
|
||||
import { createServer } from 'vite'
|
||||
|
||||
export type CapacitorPlatform = 'android' | 'ios'
|
||||
|
||||
export interface RunCapViteOptions {
|
||||
cwd?: string
|
||||
debounceMs?: number
|
||||
}
|
||||
|
||||
const nativeExtensionsByPlatform: Record<CapacitorPlatform, Set<string>> = {
|
||||
ios: new Set([
|
||||
'.entitlements',
|
||||
'.h',
|
||||
'.hpp',
|
||||
'.m',
|
||||
'.mm',
|
||||
'.pbxproj',
|
||||
'.plist',
|
||||
'.storyboard',
|
||||
'.strings',
|
||||
'.swift',
|
||||
'.xcodeproj',
|
||||
'.xcconfig',
|
||||
'.xcscheme',
|
||||
'.xib',
|
||||
]),
|
||||
android: new Set([
|
||||
'.gradle',
|
||||
'.java',
|
||||
'.json',
|
||||
'.kts',
|
||||
'.kt',
|
||||
'.properties',
|
||||
'.xml',
|
||||
]),
|
||||
}
|
||||
|
||||
const nativeNamesByPlatform: Record<CapacitorPlatform, Set<string>> = {
|
||||
ios: new Set([
|
||||
'Podfile',
|
||||
'Podfile.lock',
|
||||
'project.pbxproj',
|
||||
]),
|
||||
android: new Set([
|
||||
'AndroidManifest.xml',
|
||||
'build.gradle',
|
||||
'build.gradle.kts',
|
||||
'gradle.properties',
|
||||
'settings.gradle',
|
||||
'settings.gradle.kts',
|
||||
]),
|
||||
}
|
||||
|
||||
const ignoredNames = new Set([
|
||||
'capacitor.config.json',
|
||||
])
|
||||
|
||||
const ignoredPathSegments = new Set([
|
||||
'.gradle',
|
||||
'DerivedData',
|
||||
'Pods',
|
||||
'build',
|
||||
'xcuserdata',
|
||||
])
|
||||
|
||||
const ignoredPathPrefixesByPlatform: Record<CapacitorPlatform, string[][]> = {
|
||||
ios: [
|
||||
['App', 'CapApp-SPM'],
|
||||
],
|
||||
android: [],
|
||||
}
|
||||
|
||||
function pickServerUrl(server: ViteDevServer): URL {
|
||||
const url = server.resolvedUrls?.network?.[0] ?? server.resolvedUrls?.local?.[0]
|
||||
|
||||
if (!url) {
|
||||
throw new Error('Vite did not expose a reachable dev server URL.')
|
||||
}
|
||||
|
||||
const resolved = new URL(url)
|
||||
|
||||
return resolved
|
||||
}
|
||||
|
||||
function shouldRestartForNativeChange(file: string, platform: CapacitorPlatform, cwd: string): boolean {
|
||||
const absoluteFile = resolve(cwd, file)
|
||||
const platformRoot = resolve(cwd, platform)
|
||||
|
||||
if (!absoluteFile.startsWith(`${platformRoot}${sep}`) && absoluteFile !== platformRoot) {
|
||||
return false
|
||||
}
|
||||
|
||||
const fileName = basename(absoluteFile)
|
||||
|
||||
if (ignoredNames.has(fileName)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const segments = absoluteFile.split(sep)
|
||||
if (segments.some(segment => ignoredPathSegments.has(segment))) {
|
||||
return false
|
||||
}
|
||||
|
||||
const relativeFile = relative(platformRoot, absoluteFile)
|
||||
const relativeSegments = relativeFile.split(sep).filter(Boolean)
|
||||
|
||||
if (ignoredPathPrefixesByPlatform[platform].some(prefix =>
|
||||
prefix.every((segment, index) => relativeSegments[index] === segment),
|
||||
)) {
|
||||
// NOTICE: Capacitor regenerates ios/App/CapApp-SPM/Package.swift during `cap run`.
|
||||
// Treating that generated tree as a native source change causes an infinite restart loop.
|
||||
return false
|
||||
}
|
||||
|
||||
if (nativeNamesByPlatform[platform].has(fileName)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return nativeExtensionsByPlatform[platform].has(extname(fileName).toLowerCase())
|
||||
}
|
||||
|
||||
async function stopCapProcess(current: Result | undefined) {
|
||||
if (!current) {
|
||||
return
|
||||
}
|
||||
|
||||
current.kill('SIGINT')
|
||||
|
||||
try {
|
||||
await current
|
||||
}
|
||||
catch {
|
||||
// tinyexec rejects on interrupted exits when the child was stopped for a restart.
|
||||
}
|
||||
}
|
||||
|
||||
function startCapProcess(cwd: string, platform: CapacitorPlatform, deviceId: string) {
|
||||
return x('cap', ['run', platform, '--target', deviceId], { persist: true, throwOnError: false, nodeOptions: { cwd, stdio: 'inherit' } })
|
||||
}
|
||||
|
||||
export async function runCapVite(
|
||||
platform: CapacitorPlatform,
|
||||
deviceId: string,
|
||||
options: RunCapViteOptions = {},
|
||||
): Promise<void> {
|
||||
const cwd = resolve(options.cwd ?? process.cwd())
|
||||
const debounceMs = options.debounceMs ?? 300
|
||||
const server = await createServer({
|
||||
clearScreen: false,
|
||||
root: cwd,
|
||||
})
|
||||
|
||||
await server.listen()
|
||||
server.printUrls()
|
||||
|
||||
const url = pickServerUrl(server)
|
||||
const logger = server.config.logger
|
||||
|
||||
await x('cap', ['sync', platform], { persist: true, throwOnError: false, nodeOptions: { cwd, env: { CAPACITOR_DEV_SERVER_URL: url.toString() }, stdio: 'inherit' } })
|
||||
|
||||
let currentCapProcess: Result | undefined = startCapProcess(cwd, platform, deviceId)
|
||||
let restartTimer: NodeJS.Timeout | undefined
|
||||
let shuttingDown = false
|
||||
|
||||
async function restartCapProcess(reason: string) {
|
||||
if (shuttingDown) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.info(`[cap-vite] ${reason}. Re-running cap run ${platform}.`)
|
||||
const previous = currentCapProcess
|
||||
currentCapProcess = undefined
|
||||
await stopCapProcess(previous)
|
||||
currentCapProcess = startCapProcess(cwd, platform, deviceId)
|
||||
}
|
||||
|
||||
const onWatcherEvent = (_event: string, file: string) => {
|
||||
if (!shouldRestartForNativeChange(file, platform, cwd)) {
|
||||
return
|
||||
}
|
||||
|
||||
clearTimeout(restartTimer)
|
||||
restartTimer = setTimeout(() => {
|
||||
void restartCapProcess(`native file changed: ${resolve(cwd, file)}`)
|
||||
}, debounceMs)
|
||||
}
|
||||
|
||||
const shutdown = async (exitCode: number) => {
|
||||
if (shuttingDown) {
|
||||
return
|
||||
}
|
||||
|
||||
shuttingDown = true
|
||||
clearTimeout(restartTimer)
|
||||
server.watcher.off('all', onWatcherEvent)
|
||||
await server.watcher.unwatch(platform)
|
||||
await server.close()
|
||||
await stopCapProcess(currentCapProcess)
|
||||
process.exit(exitCode)
|
||||
}
|
||||
|
||||
server.watcher.add(platform)
|
||||
server.watcher.on('all', onWatcherEvent)
|
||||
|
||||
process.once('SIGINT', () => {
|
||||
void shutdown(0)
|
||||
})
|
||||
process.once('SIGTERM', () => {
|
||||
void shutdown(0)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"ESNext"
|
||||
],
|
||||
"types": [
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'tsdown'
|
||||
|
||||
export default defineConfig({
|
||||
entry: {
|
||||
'index': 'src/index.ts',
|
||||
'bin/run': 'src/bin/run.ts',
|
||||
},
|
||||
target: 'node18',
|
||||
outDir: 'dist',
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
})
|
||||
Generated
+18
-3
@@ -830,6 +830,9 @@ importers:
|
||||
'@intlify/unplugin-vue-i18n':
|
||||
specifier: ^11.0.7
|
||||
version: 11.0.7(@vue/compiler-dom@3.5.29)(eslint@9.39.3(jiti@2.6.1))(rollup@2.80.0)(typescript@5.9.3)(vue-i18n@11.2.8(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3))
|
||||
'@proj-airi/cap-vite':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/cap-vite
|
||||
'@proj-airi/iconify-meteocons':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.4
|
||||
@@ -1949,6 +1952,18 @@ importers:
|
||||
specifier: catalog:vitest
|
||||
version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.14)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.1.0)(canvas@3.2.1)(utf-8-validate@5.0.10))(less@4.5.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
packages/cap-vite:
|
||||
dependencies:
|
||||
'@capacitor/cli':
|
||||
specifier: ^8.0.0
|
||||
version: 8.1.0
|
||||
tinyexec:
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.2
|
||||
vite:
|
||||
specifier: ^7.0.0 || ^8.0.0-beta.0
|
||||
version: 7.3.1(@types/node@24.10.14)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
packages/ccc:
|
||||
dependencies:
|
||||
meta-png:
|
||||
@@ -3202,7 +3217,7 @@ importers:
|
||||
version: 14.1.0(vue@3.5.29(typescript@5.9.3))
|
||||
'@wxt-dev/module-vue':
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3(vite@7.3.1(@types/node@24.10.14)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))(wxt@0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
version: 1.0.3(vite@8.0.0-beta.15(@types/node@24.10.14)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))(wxt@0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
nanoid:
|
||||
specifier: ^5.1.6
|
||||
version: 5.1.6
|
||||
@@ -24078,9 +24093,9 @@ snapshots:
|
||||
'@types/filesystem': 0.0.36
|
||||
'@types/har-format': 1.2.16
|
||||
|
||||
'@wxt-dev/module-vue@1.0.3(vite@7.3.1(@types/node@24.10.14)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))(wxt@0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
'@wxt-dev/module-vue@1.0.3(vite@8.0.0-beta.15(@types/node@24.10.14)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))(wxt@0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@vitejs/plugin-vue': 6.0.4(vite@7.3.1(@types/node@24.10.14)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))
|
||||
'@vitejs/plugin-vue': 6.0.4(vite@8.0.0-beta.15(@types/node@24.10.14)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))
|
||||
wxt: 0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
transitivePeerDependencies:
|
||||
- vite
|
||||
|
||||
Reference in New Issue
Block a user