feat(stage-tamagotchi,stage-shared): listen keyup for global shortcut with uiohook
Some checks are pending
CI / Lint (push) Waiting to run
CI / Build Test (stage-tamagotchi) (push) Waiting to run
CI / Build Test (stage-tamagotchi-godot) (push) Waiting to run
CI / Build Test (stage-web) (push) Waiting to run
CI / Build Test (ui-transitions) (push) Waiting to run
CI / Build Test (ui-loading-screens) (push) Waiting to run
CI / Type Check (push) Waiting to run
CI / Check Provenance (push) Waiting to run
Cloudflare Workers / Deploy - stage-web (push) Waiting to run
Update Nix pnpmDeps Hash / update (push) Waiting to run

This commit is contained in:
Makito 2026-05-20 03:07:54 +09:00
parent 10f340a651
commit f86d86c04c
No known key found for this signature in database
GPG key ID: FF813827E1D9D2CD
10 changed files with 829 additions and 282 deletions

View file

@ -192,8 +192,10 @@
"h3": "2.0.1-rc.20",
"less": "^4.6.4",
"mkcert": "catalog:",
"node-abi": "catalog:",
"std-env": "^4.1.0",
"superjson": "catalog:",
"uiohook-napi": "catalog:",
"unocss-preset-scrollbar": "^4.0.0",
"unplugin-info": "^1.3.2",
"unplugin-yaml": "^4.1.0",

View file

@ -0,0 +1,368 @@
import type { ShortcutBinding } from '@proj-airi/stage-shared/global-shortcut'
import { ShortcutFailureReasons } from '@proj-airi/stage-shared/global-shortcut'
import { beforeEach, describe, expect, it, vi } from 'vitest'
/**
* Builds a binding for the uiohook driver.
*
* Defaults to `receiveKeyUps: true` because that flag is the dispatch
* signal in the orchestrator; the driver itself does not inspect it,
* but tests stay closer to how callers will use the driver this way.
*
* @example
* exampleBinding('ptt')
* // => { id: 'ptt', accelerator: { modifiers: ['shift'], key: 'KeyK' },
* // scope: 'global', receiveKeyUps: true }
*/
function exampleBinding(id: string, modifiers: ShortcutBinding['accelerator']['modifiers'] = ['shift'], key = 'KeyK'): ShortcutBinding {
return {
id,
accelerator: { modifiers, key },
scope: 'global',
receiveKeyUps: true,
}
}
interface KeyboardEvent {
keycode: number
altKey: boolean
ctrlKey: boolean
metaKey: boolean
shiftKey: boolean
}
function event(partial: Partial<KeyboardEvent> & Pick<KeyboardEvent, 'keycode'>): KeyboardEvent {
return {
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
...partial,
}
}
/**
* Wires mocks for `uiohook-napi` (singleton + listeners) and the
* `electron` `systemPreferences` surface, then imports the driver
* factory under test.
*
* @example
* const m = await setupMocks()
* const driver = m.createUiohookDriver({ ... })
* m.fire('keydown', event({ keycode: 37 }))
*/
async function setupMocks() {
const onMock = vi.fn()
const removeListenerMock = vi.fn()
const startMock = vi.fn()
const stopMock = vi.fn()
const isTrustedAccessibilityClientMock = vi.fn(() => true)
const listeners = new Map<string, Array<(e: KeyboardEvent) => void>>()
onMock.mockImplementation((event: string, listener: (e: KeyboardEvent) => void) => {
const arr = listeners.get(event) ?? []
arr.push(listener)
listeners.set(event, arr)
})
removeListenerMock.mockImplementation((event: string, listener: (e: KeyboardEvent) => void) => {
const arr = listeners.get(event)
if (!arr)
return
listeners.set(event, arr.filter(l => l !== listener))
})
// Mirrors the literal subset of `UiohookKey` that exercises the
// mapper. KeyK = 37, KeyA = 30 (matches real upstream constants so
// tests assert real keycodes, not arbitrary numbers).
const UiohookKey = {
K: 37,
A: 30,
Q: 16,
} as const
vi.doMock('uiohook-napi', () => ({
uIOhook: {
on: onMock,
removeListener: removeListenerMock,
start: startMock,
stop: stopMock,
},
UiohookKey,
}))
vi.doMock('electron', () => ({
systemPreferences: {
isTrustedAccessibilityClient: isTrustedAccessibilityClientMock,
},
}))
const { createUiohookDriver } = await import('./global-shortcut-uiohook')
function fire(name: 'keydown' | 'keyup', e: KeyboardEvent): void {
for (const listener of listeners.get(name) ?? [])
listener(e)
}
function createDriver(overrides: { platform?: NodeJS.Platform, sessionType?: string } = {}) {
const broadcastTriggered = vi.fn<(id: string, phase: 'down' | 'up') => void>()
const logger = {
warn: vi.fn(),
withError: vi.fn(() => ({ warn: vi.fn() })),
}
const driver = createUiohookDriver({
broadcastTriggered,
logger: logger as unknown as Parameters<typeof createUiohookDriver>[0]['logger'],
platform: overrides.platform ?? 'darwin',
sessionType: overrides.sessionType,
})
return { driver, broadcastTriggered, logger }
}
return {
onMock,
removeListenerMock,
startMock,
stopMock,
isTrustedAccessibilityClientMock,
fire,
createDriver,
}
}
describe('createUiohookDriver', () => {
beforeEach(() => {
vi.resetModules()
vi.clearAllMocks()
vi.restoreAllMocks()
})
it('starts the OS hook lazily and installs keydown/keyup listeners on first registration', async () => {
const m = await setupMocks()
const { driver } = m.createDriver()
expect(m.startMock).not.toHaveBeenCalled()
expect(m.onMock).not.toHaveBeenCalled()
const result = driver.tryRegister(exampleBinding('ptt'))
expect(result).toEqual({ id: 'ptt', ok: true })
expect(m.startMock).toHaveBeenCalledTimes(1)
expect(m.onMock).toHaveBeenCalledWith('keydown', expect.any(Function))
expect(m.onMock).toHaveBeenCalledWith('keyup', expect.any(Function))
})
it('stops the OS hook only after the last binding is unregistered', async () => {
const m = await setupMocks()
const { driver } = m.createDriver()
driver.tryRegister(exampleBinding('a', ['shift'], 'KeyA'))
driver.tryRegister(exampleBinding('b', ['shift'], 'KeyQ'))
expect(m.stopMock).not.toHaveBeenCalled()
driver.unregisterById('a')
expect(m.stopMock).not.toHaveBeenCalled()
driver.unregisterById('b')
expect(m.stopMock).toHaveBeenCalledTimes(1)
})
it('broadcasts a "down" event when a matching keydown arrives', async () => {
const m = await setupMocks()
const { driver, broadcastTriggered } = m.createDriver()
driver.tryRegister(exampleBinding('ptt', ['shift'], 'KeyK'))
m.fire('keydown', event({ keycode: 37, shiftKey: true }))
expect(broadcastTriggered).toHaveBeenCalledTimes(1)
expect(broadcastTriggered).toHaveBeenCalledWith('ptt', 'down')
})
it('suppresses OS auto-repeat — repeated keydowns between matching down/up collapse to one broadcast', async () => {
// ROOT CAUSE:
//
// libuiohook reports the OS-level keydown stream verbatim, which
// includes auto-repeat events while the key remains physically
// held. Without per-binding `pressed` tracking, a held PTT key
// would emit hundreds of `down` broadcasts per second and the mic
// would start/stop frantically.
const m = await setupMocks()
const { driver, broadcastTriggered } = m.createDriver()
driver.tryRegister(exampleBinding('ptt', ['shift'], 'KeyK'))
m.fire('keydown', event({ keycode: 37, shiftKey: true }))
m.fire('keydown', event({ keycode: 37, shiftKey: true }))
m.fire('keydown', event({ keycode: 37, shiftKey: true }))
const downCalls = broadcastTriggered.mock.calls.filter(c => c[1] === 'down')
expect(downCalls).toHaveLength(1)
})
it('broadcasts "up" on matching keyup and re-arms the binding for the next press', async () => {
const m = await setupMocks()
const { driver, broadcastTriggered } = m.createDriver()
driver.tryRegister(exampleBinding('ptt', ['shift'], 'KeyK'))
m.fire('keydown', event({ keycode: 37, shiftKey: true }))
m.fire('keyup', event({ keycode: 37, shiftKey: false }))
m.fire('keydown', event({ keycode: 37, shiftKey: true }))
expect(broadcastTriggered).toHaveBeenNthCalledWith(1, 'ptt', 'down')
expect(broadcastTriggered).toHaveBeenNthCalledWith(2, 'ptt', 'up')
expect(broadcastTriggered).toHaveBeenNthCalledWith(3, 'ptt', 'down')
})
it('matches keyup by keycode even when modifiers were released before the main key', async () => {
// NOTICE:
// Users routinely release the modifier first (e.g. let Cmd go
// before letting K go). The keyup event for K therefore carries
// `metaKey: false`, which would fail strict modifier matching.
// The driver keys the "up" broadcast off the prior `pressed`
// state rather than the modifier predicate.
const m = await setupMocks()
const { driver, broadcastTriggered } = m.createDriver()
driver.tryRegister(exampleBinding('ptt', ['cmd-or-ctrl'], 'KeyK'))
m.fire('keydown', event({ keycode: 37, metaKey: true }))
m.fire('keyup', event({ keycode: 37, metaKey: false }))
expect(broadcastTriggered).toHaveBeenCalledWith('ptt', 'down')
expect(broadcastTriggered).toHaveBeenCalledWith('ptt', 'up')
})
it('ignores keyup when no matching keydown was tracked', async () => {
const m = await setupMocks()
const { driver, broadcastTriggered } = m.createDriver()
driver.tryRegister(exampleBinding('ptt', ['shift'], 'KeyK'))
m.fire('keyup', event({ keycode: 37, shiftKey: true }))
expect(broadcastTriggered).not.toHaveBeenCalled()
})
it('does not match a keydown that carries an extra modifier', async () => {
// Strict matching mirrors Electron's accelerator semantics: a
// `Shift+K` binding must not fire on `Cmd+Shift+K`.
const m = await setupMocks()
const { driver, broadcastTriggered } = m.createDriver()
driver.tryRegister(exampleBinding('ptt', ['shift'], 'KeyK'))
m.fire('keydown', event({ keycode: 37, shiftKey: true, metaKey: true }))
expect(broadcastTriggered).not.toHaveBeenCalled()
})
it('maps cmd-or-ctrl to metaKey on darwin', async () => {
const m = await setupMocks()
const { driver, broadcastTriggered } = m.createDriver({ platform: 'darwin' })
driver.tryRegister(exampleBinding('ptt', ['cmd-or-ctrl'], 'KeyK'))
m.fire('keydown', event({ keycode: 37, metaKey: true }))
m.fire('keydown', event({ keycode: 37, ctrlKey: true }))
expect(broadcastTriggered).toHaveBeenCalledTimes(1)
expect(broadcastTriggered).toHaveBeenCalledWith('ptt', 'down')
})
it('maps cmd-or-ctrl to ctrlKey on non-darwin platforms', async () => {
const m = await setupMocks()
const { driver, broadcastTriggered } = m.createDriver({ platform: 'win32' })
driver.tryRegister(exampleBinding('ptt', ['cmd-or-ctrl'], 'KeyK'))
m.fire('keydown', event({ keycode: 37, ctrlKey: true }))
m.fire('keydown', event({ keycode: 37, metaKey: true }))
// First (ctrl) matches; second (meta) does not — the pressed
// state stays cleared and produces no extra broadcast.
expect(broadcastTriggered).toHaveBeenCalledTimes(1)
expect(broadcastTriggered).toHaveBeenCalledWith('ptt', 'down')
})
it('rejects duplicate ids with reason "duplicate-id" and does not double-start the hook', async () => {
const m = await setupMocks()
const { driver } = m.createDriver()
expect(driver.tryRegister(exampleBinding('ptt'))).toEqual({ id: 'ptt', ok: true })
const second = driver.tryRegister(exampleBinding('ptt'))
expect(second).toEqual({ id: 'ptt', ok: false, reason: ShortcutFailureReasons.DuplicateId })
expect(m.startMock).toHaveBeenCalledTimes(1)
})
it('returns Unsupported under a native Wayland session', async () => {
const m = await setupMocks()
const { driver } = m.createDriver({ platform: 'linux', sessionType: 'wayland' })
const result = driver.tryRegister(exampleBinding('ptt'))
expect(result).toEqual({ id: 'ptt', ok: false, reason: ShortcutFailureReasons.Unsupported })
expect(m.startMock).not.toHaveBeenCalled()
})
it('permits registration on Linux under X11 / XWayland', async () => {
const m = await setupMocks()
const { driver } = m.createDriver({ platform: 'linux', sessionType: 'x11' })
expect(driver.tryRegister(exampleBinding('ptt'))).toEqual({ id: 'ptt', ok: true })
expect(m.startMock).toHaveBeenCalledTimes(1)
})
it('returns Denied when macOS Accessibility permission is not granted', async () => {
const m = await setupMocks()
m.isTrustedAccessibilityClientMock.mockReturnValue(false)
const { driver } = m.createDriver({ platform: 'darwin' })
const result = driver.tryRegister(exampleBinding('ptt'))
expect(result).toEqual({ id: 'ptt', ok: false, reason: ShortcutFailureReasons.Denied })
expect(m.isTrustedAccessibilityClientMock).toHaveBeenCalledWith(true)
expect(m.startMock).not.toHaveBeenCalled()
})
it('skips the Accessibility check entirely on non-darwin', async () => {
const m = await setupMocks()
const { driver } = m.createDriver({ platform: 'win32' })
driver.tryRegister(exampleBinding('ptt'))
expect(m.isTrustedAccessibilityClientMock).not.toHaveBeenCalled()
})
it('unregisterAll clears every binding and stops the hook in one shot', async () => {
const m = await setupMocks()
const { driver, broadcastTriggered } = m.createDriver()
driver.tryRegister(exampleBinding('a', ['shift'], 'KeyA'))
driver.tryRegister(exampleBinding('b', ['shift'], 'KeyQ'))
driver.unregisterAll()
m.fire('keydown', event({ keycode: 30, shiftKey: true }))
m.fire('keydown', event({ keycode: 16, shiftKey: true }))
expect(broadcastTriggered).not.toHaveBeenCalled()
expect(m.stopMock).toHaveBeenCalledTimes(1)
})
it('dispose removes the keydown/keyup listeners', async () => {
const m = await setupMocks()
const { driver } = m.createDriver()
driver.tryRegister(exampleBinding('ptt'))
driver.dispose()
expect(m.removeListenerMock).toHaveBeenCalledWith('keydown', expect.any(Function))
expect(m.removeListenerMock).toHaveBeenCalledWith('keyup', expect.any(Function))
})
it('keeps per-binding pressed state independent across multiple bindings', async () => {
const m = await setupMocks()
const { driver, broadcastTriggered } = m.createDriver()
driver.tryRegister(exampleBinding('a', ['shift'], 'KeyA'))
driver.tryRegister(exampleBinding('b', ['shift'], 'KeyQ'))
m.fire('keydown', event({ keycode: 30, shiftKey: true }))
m.fire('keydown', event({ keycode: 16, shiftKey: true }))
m.fire('keyup', event({ keycode: 30 }))
expect(broadcastTriggered).toHaveBeenNthCalledWith(1, 'a', 'down')
expect(broadcastTriggered).toHaveBeenNthCalledWith(2, 'b', 'down')
expect(broadcastTriggered).toHaveBeenNthCalledWith(3, 'a', 'up')
})
})

View file

@ -0,0 +1,317 @@
import type { useLogg } from '@guiiai/logg'
import type {
ShortcutAccelerator,
ShortcutBinding,
ShortcutKey,
ShortcutModifier,
ShortcutRegistrationResult,
} from '@proj-airi/stage-shared/global-shortcut'
import type { UiohookKeyboardEvent } from 'uiohook-napi'
import process from 'node:process'
import { ShortcutFailureReasons } from '@proj-airi/stage-shared/global-shortcut'
import { systemPreferences } from 'electron'
import { uIOhook, UiohookKey } from 'uiohook-napi'
type Logger = ReturnType<ReturnType<typeof useLogg>['useGlobalConfig']>
interface ModifierMask {
ctrl: boolean
shift: boolean
alt: boolean
meta: boolean
}
interface UiohookEntry {
binding: ShortcutBinding
predicate: (event: UiohookKeyboardEvent) => boolean
expectedKeycode: number
pressed: boolean
}
const W3C_TO_UIOHOOK: Readonly<Record<ShortcutKey, number>> = buildKeycodeMap()
function buildKeycodeMap(): Record<ShortcutKey, number> {
const map: Record<string, number> = {}
for (let i = 0; i < 26; i++) {
const letter = String.fromCharCode(65 + i)
map[`Key${letter}`] = (UiohookKey as unknown as Record<string, number>)[letter]
}
for (let i = 0; i <= 9; i++) {
map[`Digit${i}`] = (UiohookKey as unknown as Record<string, number>)[String(i)]
}
for (let i = 1; i <= 24; i++) {
map[`F${i}`] = (UiohookKey as unknown as Record<string, number>)[`F${i}`]
}
const named: Record<string, keyof typeof UiohookKey> = {
Space: 'Space',
Tab: 'Tab',
Enter: 'Enter',
Escape: 'Escape',
Backspace: 'Backspace',
Delete: 'Delete',
Insert: 'Insert',
ArrowUp: 'ArrowUp',
ArrowDown: 'ArrowDown',
ArrowLeft: 'ArrowLeft',
ArrowRight: 'ArrowRight',
Home: 'Home',
End: 'End',
PageUp: 'PageUp',
PageDown: 'PageDown',
Backquote: 'Backquote',
Minus: 'Minus',
Equal: 'Equal',
BracketLeft: 'BracketLeft',
BracketRight: 'BracketRight',
Backslash: 'Backslash',
Semicolon: 'Semicolon',
Quote: 'Quote',
Comma: 'Comma',
Period: 'Period',
Slash: 'Slash',
}
for (const [w3c, uioName] of Object.entries(named))
map[w3c] = (UiohookKey as unknown as Record<string, number>)[uioName as string]
return map
}
function resolveModifierMask(modifiers: readonly ShortcutModifier[], platform: NodeJS.Platform): ModifierMask {
const mask: ModifierMask = { ctrl: false, shift: false, alt: false, meta: false }
for (const m of modifiers) {
switch (m) {
case 'cmd-or-ctrl':
if (platform === 'darwin')
mask.meta = true
else
mask.ctrl = true
break
case 'cmd':
case 'super':
// libuiohook surfaces macOS Cmd, Windows key, and X11 Super
// through the same `metaKey` flag.
mask.meta = true
break
case 'ctrl':
mask.ctrl = true
break
case 'alt':
mask.alt = true
break
case 'shift':
mask.shift = true
break
}
}
return mask
}
function buildPredicate(acc: ShortcutAccelerator, platform: NodeJS.Platform): { predicate: UiohookEntry['predicate'], expectedKeycode: number } | undefined {
const expectedKeycode = W3C_TO_UIOHOOK[acc.key]
if (expectedKeycode === undefined)
return undefined
const required = resolveModifierMask(acc.modifiers, platform)
const predicate: UiohookEntry['predicate'] = e =>
e.keycode === expectedKeycode
&& e.ctrlKey === required.ctrl
&& e.shiftKey === required.shift
&& e.altKey === required.alt
&& e.metaKey === required.meta
return { predicate, expectedKeycode }
}
function isNativeWayland(platform: NodeJS.Platform, sessionType: string | undefined): boolean {
return platform === 'linux' && sessionType === 'wayland'
}
function isMacAccessibilityTrusted(platform: NodeJS.Platform, prompt: boolean): boolean {
if (platform !== 'darwin')
return true
try {
return systemPreferences.isTrustedAccessibilityClient(prompt)
}
catch {
return true
}
}
export interface UiohookDriverOptions {
broadcastTriggered: (id: string, phase: 'down' | 'up') => void
logger: Logger
/**
* Host platform; injected so tests can exercise cross-platform
* modifier mapping without stubbing `process`.
*
* @default process.platform
*/
platform?: NodeJS.Platform
/**
* `XDG_SESSION_TYPE` value used for the Wayland refusal check;
* injected for the same reason as `platform`.
*
* @default process.env.XDG_SESSION_TYPE
*/
sessionType?: string
}
export interface UiohookDriver {
tryRegister: (binding: ShortcutBinding) => ShortcutRegistrationResult
unregisterById: (id: string) => void
unregisterAll: () => void
dispose: () => void
}
/**
* Driver that captures global key-down and key-up events via
* libuiohook (through `uiohook-napi`).
*
* Use when:
* - A binding asks for `receiveKeyUps: true` (push-to-talk and similar
* hold-driven flows)
*
* Lifecycle:
* - The OS hook starts lazily on the first successful registration and
* stops once the last binding is unregistered, so apps that never
* bind a PTT shortcut never pay the permission/perf cost.
*
* Constraints:
* - macOS: requires the Accessibility permission. First registration
* triggers the system prompt; subsequent failures return `Denied`.
* - Linux: requires X11 or XWayland. Native Wayland sessions return
* `Unsupported` because XRecord cannot observe Wayland clients.
*/
export function createUiohookDriver(options: UiohookDriverOptions): UiohookDriver {
const {
broadcastTriggered,
logger,
platform = process.platform,
sessionType = process.env.XDG_SESSION_TYPE,
} = options
const entries = new Map<string, UiohookEntry>()
let started = false
let listenersInstalled = false
function ensureListeners(): void {
if (listenersInstalled)
return
listenersInstalled = true
uIOhook.on('keydown', onKeydown)
uIOhook.on('keyup', onKeyup)
}
function startIfNeeded(): void {
if (started || entries.size === 0)
return
try {
uIOhook.start()
started = true
}
catch (error) {
logger.withError(error).warn('Failed to start uIOhook')
}
}
function stopIfIdle(): void {
if (!started || entries.size > 0)
return
try {
uIOhook.stop()
}
catch (error) {
logger.withError(error).warn('Failed to stop uIOhook')
}
started = false
}
function onKeydown(event: UiohookKeyboardEvent): void {
for (const entry of entries.values()) {
if (!entry.predicate(event))
continue
// Auto-repeat suppression: OS may deliver repeated keydown
// while the key stays physically held. Emit one `down` per
// physical press until the matching keyup clears the flag.
if (entry.pressed)
continue
entry.pressed = true
broadcastTriggered(entry.binding.id, 'down')
}
}
function onKeyup(event: UiohookKeyboardEvent): void {
// NOTICE:
// Match keyup by keycode alone; modifier flags may be released
// before the main key (e.g. Cmd released before K), in which case
// the strict predicate would not match. Pairing keyup to the
// binding via the prior `pressed` state ensures every `down`
// emits a matching `up`.
for (const entry of entries.values()) {
if (!entry.pressed)
continue
if (event.keycode !== entry.expectedKeycode)
continue
entry.pressed = false
broadcastTriggered(entry.binding.id, 'up')
}
}
function tryRegister(binding: ShortcutBinding): ShortcutRegistrationResult {
if (entries.has(binding.id))
return { id: binding.id, ok: false, reason: ShortcutFailureReasons.DuplicateId }
if (isNativeWayland(platform, sessionType)) {
// libuiohook hooks install but never receive events under
// native Wayland. Refuse rather than register a binding that
// would silently no-op.
return { id: binding.id, ok: false, reason: ShortcutFailureReasons.Unsupported }
}
if (!isMacAccessibilityTrusted(platform, true))
return { id: binding.id, ok: false, reason: ShortcutFailureReasons.Denied }
const built = buildPredicate(binding.accelerator, platform)
if (built === undefined) {
logger.warn(`uiohook driver: no keycode mapping for "${binding.accelerator.key}"`)
return { id: binding.id, ok: false, reason: ShortcutFailureReasons.Unsupported }
}
entries.set(binding.id, {
binding,
predicate: built.predicate,
expectedKeycode: built.expectedKeycode,
pressed: false,
})
ensureListeners()
startIfNeeded()
return { id: binding.id, ok: true }
}
function unregisterById(id: string): void {
if (!entries.delete(id))
return
stopIfIdle()
}
function unregisterAll(): void {
if (entries.size === 0)
return
entries.clear()
stopIfIdle()
}
function dispose(): void {
unregisterAll()
if (listenersInstalled) {
uIOhook.removeListener('keydown', onKeydown)
uIOhook.removeListener('keyup', onKeyup)
listenersInstalled = false
}
}
return { tryRegister, unregisterById, unregisterAll, dispose }
}

View file

@ -101,6 +101,19 @@ async function setupMocks() {
unregister: unregisterMock,
unregisterAll: unregisterAllMock,
},
systemPreferences: {
isTrustedAccessibilityClient: vi.fn(() => true),
},
}))
vi.doMock('uiohook-napi', () => ({
uIOhook: {
on: vi.fn(),
removeListener: vi.fn(),
start: vi.fn(),
stop: vi.fn(),
},
UiohookKey: new Proxy({}, { get: () => 0 }),
}))
vi.doMock('@moeru/eventa', async (importOriginal) => {
@ -162,19 +175,15 @@ describe('setupGlobalShortcutService', () => {
expect(m.registerMock).toHaveBeenCalledWith('CmdOrCtrl+Shift+K', expect.any(Function))
})
it('refuses receiveKeyUps with reason "unsupported" and does not call globalShortcut', async () => {
// Electron's `globalShortcut` does not deliver key-release
// events. The driver refuses `receiveKeyUps: true` honestly so
// callers can switch to (or fail back from) the uiohook driver
// path that will handle it.
it('routes receiveKeyUps:true to the uiohook driver and bypasses electron.globalShortcut', async () => {
const m = await setupMocks()
const service = m.setupGlobalShortcutService()
const ctx = createMockContext()
registerMockWindow(service, ctx)
const handler = ctx.invokeHandlers.get('eventa:invoke:electron:shortcut:register')!
const result = handler({ ...exampleBinding('ptt'), receiveKeyUps: true }) as { id: string, ok: boolean, reason?: string }
expect(result).toEqual({ id: 'ptt', ok: false, reason: ShortcutFailureReasons.Unsupported })
const result = handler({ ...exampleBinding('ptt'), receiveKeyUps: true }) as { id: string, ok: boolean }
expect(result).toEqual({ id: 'ptt', ok: true })
expect(m.registerMock).not.toHaveBeenCalled()
})

View file

@ -15,6 +15,7 @@ import {
electronShortcutUnregisterAll,
} from '../../../shared/eventa'
import { onAppBeforeQuit } from '../../libs/bootkit/lifecycle'
import { createUiohookDriver } from './global-shortcut-uiohook'
export type EventaContext = ReturnType<typeof createContext>['context']
@ -34,10 +35,10 @@ export interface GlobalShortcutService {
dispose: () => void
}
interface ActiveBinding {
binding: ShortcutBinding
electronAccelerator: string
}
type ActiveBinding = { binding: ShortcutBinding } & (
| { driver: 'electron', electronAccelerator: string }
| { driver: 'uiohook' }
)
export function setupGlobalShortcutService(): GlobalShortcutService {
const log = useLogg('global-shortcut').useGlobalConfig()
@ -56,20 +57,12 @@ export function setupGlobalShortcutService(): GlobalShortcutService {
}
}
function tryRegister(binding: ShortcutBinding): ShortcutRegistrationResult {
if (binding.receiveKeyUps) {
// Electron's `globalShortcut` only fires on press. A separate
// driver path (uiohook-napi) handles `receiveKeyUps: true`;
// this driver refuses honestly until that path is wired.
return { id: binding.id, ok: false, reason: ShortcutFailureReasons.Unsupported }
}
if (active.has(binding.id)) {
// Callers must `unregister` first to rebind. Avoids silent overrides
// between unrelated registration sites.
return { id: binding.id, ok: false, reason: ShortcutFailureReasons.DuplicateId }
}
const uiohookDriver = createUiohookDriver({
broadcastTriggered,
logger: log,
})
function tryRegisterElectron(binding: ShortcutBinding): ShortcutRegistrationResult {
const electronAccelerator = formatElectronAccelerator(binding.accelerator)
const ok = globalShortcut.register(electronAccelerator, () => broadcastTriggered(binding.id, 'down'))
@ -78,30 +71,37 @@ export function setupGlobalShortcutService(): GlobalShortcutService {
// causes (held by another app, or denied by the OS for media
// keys / Accessibility-gated combos on macOS). Electron does not
// expose which case applied, so this driver reports `Conflict`
// for both. A future driver path (XDG portal, native macOS) can
// emit `Denied` directly.
// for both. The uiohook driver path can emit `Denied` directly.
return { id: binding.id, ok: false, reason: ShortcutFailureReasons.Conflict }
}
active.set(binding.id, { binding, electronAccelerator })
active.set(binding.id, { binding, driver: 'electron', electronAccelerator })
return { id: binding.id, ok: true }
}
function tryRegisterUiohook(binding: ShortcutBinding): ShortcutRegistrationResult {
const result = uiohookDriver.tryRegister(binding)
if (result.ok)
active.set(binding.id, { binding, driver: 'uiohook' })
return result
}
function tryRegister(binding: ShortcutBinding): ShortcutRegistrationResult {
if (active.has(binding.id)) {
return { id: binding.id, ok: false, reason: ShortcutFailureReasons.DuplicateId }
}
return binding.receiveKeyUps
? tryRegisterUiohook(binding)
: tryRegisterElectron(binding)
}
function unregisterById(id: string): void {
const entry = active.get(id)
if (!entry)
return
try {
globalShortcut.unregister(entry.electronAccelerator)
}
catch (error) {
log.withError(error).warn(`Failed to unregister accelerator for "${id}"`)
}
active.delete(id)
}
function unregisterAll(): void {
for (const [id, entry] of active) {
if (entry.driver === 'electron') {
try {
globalShortcut.unregister(entry.electronAccelerator)
}
@ -109,6 +109,24 @@ export function setupGlobalShortcutService(): GlobalShortcutService {
log.withError(error).warn(`Failed to unregister accelerator for "${id}"`)
}
}
else {
uiohookDriver.unregisterById(id)
}
active.delete(id)
}
function unregisterAll(): void {
for (const [id, entry] of active) {
if (entry.driver !== 'electron')
continue
try {
globalShortcut.unregister(entry.electronAccelerator)
}
catch (error) {
log.withError(error).warn(`Failed to unregister accelerator for "${id}"`)
}
}
uiohookDriver.unregisterAll()
active.clear()
}
@ -142,6 +160,7 @@ export function setupGlobalShortcutService(): GlobalShortcutService {
const dispose: GlobalShortcutService['dispose'] = () => {
unregisterAll()
uiohookDriver.dispose()
contexts.clear()
}

View file

@ -144,6 +144,18 @@ function formatTime(ms: number) {
return new Date(ms).toLocaleTimeString()
}
function deltaForTrigger(index: number): string {
const current = triggers.value[index]
if (!current)
return ''
for (let i = index + 1; i < triggers.value.length; i++) {
const prev = triggers.value[i]
if (prev.id === current.id)
return String(current.time - prev.time)
}
return ''
}
let disposeTriggerListener: (() => void) | undefined
onMounted(async () => {
@ -195,7 +207,7 @@ onUnmounted(() => {
<FieldCheckbox
v-model="form.receiveKeyUps"
label="Receive key-ups"
description="Asks the driver to also emit on release. Electron driver refuses with 'unsupported'."
description="Routes the binding through the uiohook driver so both 'down' and 'up' fire. macOS needs Accessibility; native Wayland returns 'unsupported'."
/>
</div>
<div class="flex flex-wrap gap-3">
@ -349,6 +361,9 @@ onUnmounted(() => {
<th class="px-3 py-2 text-left">
Phase
</th>
<th class="px-3 py-2 text-left">
Δ ms
</th>
</tr>
</thead>
<tbody>
@ -366,6 +381,9 @@ onUnmounted(() => {
<td class="px-3 py-2 font-mono">
{{ t.phase }}
</td>
<td class="px-3 py-2 text-neutral-500 font-mono dark:text-neutral-400">
{{ deltaForTrigger(i) }}
</td>
</tr>
</tbody>
</table>

View file

@ -161,6 +161,7 @@ words:
- lerp
- libsamplerate
- libsodium
- libuiohook
- lightningcss
- lipsync
- listhen

View file

@ -62,10 +62,12 @@ export interface ShortcutBinding {
/**
* Whether the driver should also emit key-release events.
*
* Drivers that cannot deliver release events refuse the registration
* with `{ ok: false, reason: ShortcutFailureReasons.Unsupported }`. The Electron
* `globalShortcut` driver currently refuses; a uiohook-based driver
* path is planned to honour this flag.
* Routes the binding through the uiohook driver, which delivers
* both `down` and `up` phases. Required for push-to-talk and any
* hold-driven flow. Drivers that genuinely cannot deliver release
* events under the current session (e.g. native Wayland for the
* uiohook path) refuse with `{ ok: false, reason: ShortcutFailureReasons.Unsupported }`;
* macOS without Accessibility permission refuses with `Denied`.
*
* @default false
*/

288
pnpm-lock.yaml generated
View file

@ -276,6 +276,9 @@ catalogs:
nanoid:
specifier: 5.1.6
version: 5.1.6
node-abi:
specifier: ^4.31.0
version: 4.31.0
node-pty:
specifier: ^1.1.0
version: 1.1.0
@ -321,6 +324,9 @@ catalogs:
tsx:
specifier: ^4.21.0
version: 4.21.0
uiohook-napi:
specifier: ^1.5.5
version: 1.5.5
uncrypto:
specifier: ^0.1.3
version: 0.1.3
@ -1471,7 +1477,7 @@ importers:
version: 3.0.2(electron@41.2.1)
'@electron-toolkit/tsconfig':
specifier: ^2.0.0
version: 2.0.0(@types/node@24.12.2)
version: 2.0.0(@types/node@25.6.0)
'@electron-toolkit/utils':
specifier: ^4.0.0
version: 4.0.0(electron@41.2.1)
@ -1510,7 +1516,7 @@ importers:
version: 3.1.0
'@intlify/unplugin-vue-i18n':
specifier: ^11.0.7
version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
'@modelcontextprotocol/sdk':
specifier: 'catalog:'
version: 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6)
@ -1546,10 +1552,10 @@ importers:
version: link:../../packages/ui-transitions
'@proj-airi/unplugin-fetch':
specifier: 'catalog:'
version: 0.2.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
version: 0.2.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
'@proj-airi/unplugin-live2d-sdk':
specifier: ^0.1.7
version: 0.1.7(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
version: 0.1.7(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
'@types/audioworklet':
specifier: 'catalog:'
version: 0.0.97
@ -1576,7 +1582,7 @@ importers:
version: 2.10.3
'@vitejs/plugin-vue':
specifier: ^6.0.6
version: 6.0.6(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
version: 6.0.6(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
'@vue-macros/volar':
specifier: ^3.1.2
version: 3.1.2(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
@ -1609,7 +1615,7 @@ importers:
version: 6.8.3
electron-vite:
specifier: ^5.0.0
version: 5.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
version: 5.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
get-port-please:
specifier: 'catalog:'
version: 3.2.0
@ -1622,39 +1628,45 @@ importers:
mkcert:
specifier: 'catalog:'
version: 3.2.0
node-abi:
specifier: 'catalog:'
version: 4.31.0
std-env:
specifier: ^4.1.0
version: 4.1.0
superjson:
specifier: 'catalog:'
version: 2.2.6
uiohook-napi:
specifier: 'catalog:'
version: 1.5.5
unocss-preset-scrollbar:
specifier: ^4.0.0
version: 4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))
version: 4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))
unplugin-info:
specifier: ^1.3.2
version: 1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
version: 1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
unplugin-yaml:
specifier: ^4.1.0
version: 4.1.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
version: 4.1.0(@nuxt/kit@3.20.2(magicast@0.5.2))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vite:
specifier: 'catalog:'
version: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vite-bundle-visualizer:
specifier: ^1.2.1
version: 1.2.1(rolldown@1.0.0-rc.16)(rollup@4.60.1)
vite-plugin-mkcert:
specifier: 'catalog:'
version: 2.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
version: 2.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vite-plugin-vue-devtools:
specifier: ^8.1.1
version: 8.1.1(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
version: 8.1.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
vite-plugin-vue-layouts:
specifier: ^0.11.0
version: 0.11.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
version: 0.11.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
vue-macros:
specifier: ^3.1.2
version: 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
version: 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
vue-tsc:
specifier: ^3.2.6
version: 3.2.6(typescript@5.9.3)
@ -15122,8 +15134,8 @@ packages:
resolution: {integrity: sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==}
engines: {node: '>=10'}
node-abi@4.24.0:
resolution: {integrity: sha512-u2EC1CeNe25uVtX3EZbdQ275c74zdZmmpzrHEQh2aIYqoVjlglfUpOX9YY85x1nlBydEKDVaSmMNhR7N82Qj8A==}
node-abi@4.31.0:
resolution: {integrity: sha512-Erq5w/t3syw3s4sDsUaX4QttIdBPsGKTT1DTRsCkTonGggczhlDKm/wDX3o+HPJpQ41EjXCbcmXf0tgr5YZJXw==}
engines: {node: '>=22.12.0'}
node-addon-api@1.7.2:
@ -17213,6 +17225,10 @@ packages:
uint4@0.1.2:
resolution: {integrity: sha512-lhEx78gdTwFWG+mt6cWAZD/R6qrIj0TTBeH5xwyuDJyswLNlGe+KVlUPQ6+mx5Ld332pS0AMUTo9hIly7YsWxQ==}
uiohook-napi@1.5.5:
resolution: {integrity: sha512-oSlTdnECw2GBfsJPTbBQBeE4v/EXP0EZmX6BJq5nzH/JgFaBE8JpFwEA/kLhiEP7HxQw28FViWiYgdIZzWuuJQ==}
engines: {node: '>= 16'}
unbash@2.2.0:
resolution: {integrity: sha512-X2wH19RAPZE3+ldGicOkoj/SIA83OIxcJ6Cuaw23hf8Xc6fQpvZXY0SftE2JgS0QhYLUG4uwodSI3R53keyh7w==}
engines: {node: '>=14'}
@ -19947,9 +19963,9 @@ snapshots:
dependencies:
electron: 41.2.1
'@electron-toolkit/tsconfig@2.0.0(@types/node@24.12.2)':
'@electron-toolkit/tsconfig@2.0.0(@types/node@25.6.0)':
dependencies:
'@types/node': 24.12.2
'@types/node': 25.6.0
'@electron-toolkit/utils@4.0.0(electron@41.2.1)':
dependencies:
@ -20028,7 +20044,7 @@ snapshots:
detect-libc: 2.1.2
got: 11.8.6
graceful-fs: 4.2.11
node-abi: 4.24.0
node-abi: 4.31.0
node-api-version: 0.2.1
node-gyp: 11.5.0
ora: 5.4.1
@ -20861,31 +20877,6 @@ snapshots:
- supports-color
- typescript
'@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
'@intlify/bundle-utils': 11.0.7(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))
'@intlify/shared': 11.3.2
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.3.2)(@vue/compiler-dom@3.5.32)(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
'@typescript-eslint/scope-manager': 8.58.1
'@typescript-eslint/typescript-estree': 8.58.1(typescript@5.9.3)
debug: 4.4.3(supports-color@10.2.2)
fast-glob: 3.3.3
pathe: 2.0.3
picocolors: 1.1.1
unplugin: 2.3.11
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vue: 3.5.32(typescript@5.9.3)
optionalDependencies:
vue-i18n: 11.3.2(vue@3.5.32(typescript@5.9.3))
transitivePeerDependencies:
- '@vue/compiler-dom'
- eslint
- rollup
- supports-color
- typescript
'@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
@ -22867,35 +22858,11 @@ snapshots:
transitivePeerDependencies:
- magicast
'@proj-airi/unplugin-fetch@0.2.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
dependencies:
ofetch: 1.5.1
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
'@proj-airi/unplugin-fetch@0.2.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
dependencies:
ofetch: 1.5.1
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
'@proj-airi/unplugin-live2d-sdk@0.1.7(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)':
dependencies:
ofetch: 1.5.1
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
yauzl: 3.3.0
transitivePeerDependencies:
- '@types/node'
- '@vitejs/devtools'
- esbuild
- jiti
- less
- sass
- sass-embedded
- stylus
- sugarss
- terser
- tsx
- yaml
'@proj-airi/unplugin-live2d-sdk@0.1.7(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)':
dependencies:
ofetch: 1.5.1
@ -23796,7 +23763,6 @@ snapshots:
'@types/node@25.6.0':
dependencies:
undici-types: 7.19.2
optional: true
'@types/nprogress@0.2.3': {}
@ -24462,12 +24428,6 @@ snapshots:
vite: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vue: 3.5.32(typescript@5.9.3)
'@vitejs/plugin-vue@6.0.6(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))':
dependencies:
'@rolldown/pluginutils': 1.0.0-rc.13
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vue: 3.5.32(typescript@5.9.3)
'@vitejs/plugin-vue@6.0.6(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))':
dependencies:
'@rolldown/pluginutils': 1.0.0-rc.13
@ -24770,15 +24730,6 @@ snapshots:
transitivePeerDependencies:
- vue
'@vue-macros/devtools@3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
dependencies:
sirv: 3.0.2
vue: 3.5.32(typescript@5.9.3)
optionalDependencies:
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
transitivePeerDependencies:
- typescript
'@vue-macros/devtools@3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
dependencies:
sirv: 3.0.2
@ -26920,7 +26871,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
electron-vite@5.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
electron-vite@5.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
'@babel/core': 7.29.0
'@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0)
@ -26928,7 +26879,7 @@ snapshots:
esbuild: 0.25.12
magic-string: 0.30.21
picocolors: 1.1.1
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
transitivePeerDependencies:
- supports-color
@ -30257,7 +30208,7 @@ snapshots:
dependencies:
semver: 7.7.4
node-abi@4.24.0:
node-abi@4.31.0:
dependencies:
semver: 7.7.4
@ -32809,6 +32760,10 @@ snapshots:
uint4@0.1.2: {}
uiohook-napi@1.5.5:
dependencies:
node-gyp-build: 4.8.4
unbash@2.2.0: {}
unconfig-core@7.5.0:
@ -32840,8 +32795,7 @@ snapshots:
undici-types@7.16.0: {}
undici-types@7.19.2:
optional: true
undici-types@7.19.2: {}
undici@6.24.1: {}
@ -32953,6 +32907,11 @@ snapshots:
'@unocss/preset-mini': 66.6.8
unocss: 66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
unocss-preset-scrollbar@4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))):
dependencies:
'@unocss/preset-mini': 66.6.8
unocss: 66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
'@unocss/cli': 66.6.8
@ -33049,14 +33008,6 @@ snapshots:
unplugin: 2.3.11
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
unplugin-combine@2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
optionalDependencies:
esbuild: 0.27.2
rolldown: 1.0.0-rc.16
rollup: 4.60.1
unplugin: 2.3.11
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
unplugin-combine@2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
optionalDependencies:
esbuild: 0.27.2
@ -33091,19 +33042,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
unplugin-info@1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
ci-info: 4.4.0
git-url-parse: 16.1.0
simple-git: 3.36.0
unplugin: 2.3.11
optionalDependencies:
esbuild: 0.27.2
rollup: 4.60.1
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
transitivePeerDependencies:
- supports-color
unplugin-info@1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
ci-info: 4.4.0
@ -33213,17 +33151,6 @@ snapshots:
rollup: 4.60.1
vite: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
unplugin-yaml@4.1.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
unplugin: 3.0.0
yaml: 2.8.3
optionalDependencies:
esbuild: 0.27.2
rolldown: 1.0.0-rc.16
rollup: 4.60.1
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
unplugin@2.3.11:
dependencies:
'@jridgewell/remapping': 2.3.5
@ -33455,22 +33382,12 @@ snapshots:
- rollup
- supports-color
vite-dev-rpc@1.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
birpc: 2.9.0
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vite-hot-client: 2.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vite-dev-rpc@1.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
birpc: 2.9.0
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vite-hot-client: 2.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vite-hot-client@2.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vite-hot-client@2.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
@ -33516,21 +33433,6 @@ snapshots:
- tsx
- yaml
vite-plugin-inspect@11.3.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
ansis: 4.2.0
debug: 4.4.3(supports-color@10.2.2)
error-stack-parser-es: 1.0.5
ohash: 2.0.11
open: 10.2.0
perfect-debounce: 2.1.0
sirv: 3.0.2
unplugin-utils: 0.3.1
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vite-dev-rpc: 1.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
transitivePeerDependencies:
- supports-color
vite-plugin-inspect@11.3.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
ansis: 4.2.0
@ -33562,13 +33464,6 @@ snapshots:
- typescript
- ws
vite-plugin-mkcert@2.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
debug: 4.4.3(supports-color@10.2.2)
supports-color: 10.2.2
undici: 8.1.0
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vite-plugin-mkcert@2.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
debug: 4.4.3(supports-color@10.2.2)
@ -33587,20 +33482,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
vite-plugin-vue-devtools@8.1.1(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)):
dependencies:
'@vue/devtools-core': 8.1.1(vue@3.5.32(typescript@5.9.3))
'@vue/devtools-kit': 8.1.1
'@vue/devtools-shared': 8.1.1
sirv: 3.0.2
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vite-plugin-inspect: 11.3.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vite-plugin-vue-inspector: 5.3.2(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
transitivePeerDependencies:
- '@nuxt/kit'
- supports-color
- vue
vite-plugin-vue-devtools@8.1.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)):
dependencies:
'@vue/devtools-core': 8.1.1(vue@3.5.32(typescript@5.9.3))
@ -33615,21 +33496,6 @@ snapshots:
- supports-color
- vue
vite-plugin-vue-inspector@5.3.2(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
'@babel/core': 7.29.0
'@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.29.0)
'@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0)
'@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0)
'@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.29.0)
'@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.0)
'@vue/compiler-dom': 3.5.32
kolorist: 1.8.0
magic-string: 0.30.21
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
transitivePeerDependencies:
- supports-color
vite-plugin-vue-inspector@5.3.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
dependencies:
'@babel/core': 7.29.0
@ -33645,16 +33511,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
vite-plugin-vue-layouts@0.11.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)):
dependencies:
debug: 4.4.3(supports-color@10.2.2)
fast-glob: 3.3.3
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vue: 3.5.32(typescript@5.9.3)
vue-router: 5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
transitivePeerDependencies:
- supports-color
vite-plugin-vue-layouts@0.11.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)):
dependencies:
debug: 4.4.3(supports-color@10.2.2)
@ -33921,54 +33777,6 @@ snapshots:
- vue-tsc
- webpack
vue-macros@3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)):
dependencies:
'@vue-macros/better-define': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3))
'@vue-macros/boolean-prop': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/chain-call': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/common': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/config': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/define-emit': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/define-models': 3.1.2(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
'@vue-macros/define-prop': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3))
'@vue-macros/define-props': 3.1.2(@vue-macros/reactivity-transform@3.1.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
'@vue-macros/define-props-refs': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/define-render': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/define-slots': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/define-stylex': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/devtools': 3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
'@vue-macros/export-expose': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/export-props': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/export-render': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/hoist-static': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/jsx-directive': 3.1.2(typescript@5.9.3)
'@vue-macros/named-template': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/reactivity-transform': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/script-lang': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/setup-block': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/setup-component': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/setup-sfc': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/short-bind': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/short-emits': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/short-vmodel': 3.1.2(vue@3.5.32(typescript@5.9.3))
'@vue-macros/volar': 3.1.2(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
unplugin: 2.3.11
unplugin-combine: 2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
unplugin-vue-define-options: 3.1.2(vue@3.5.32(typescript@5.9.3))
vue: 3.5.32(typescript@5.9.3)
transitivePeerDependencies:
- '@emnapi/core'
- '@emnapi/runtime'
- '@rspack/core'
- '@vueuse/core'
- esbuild
- rolldown
- rollup
- typescript
- vite
- vue-tsc
- webpack
vue-macros@3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)):
dependencies:
'@vue-macros/better-define': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3))

View file

@ -120,6 +120,7 @@ catalog:
mkcert: ^3.2.0
nano-staged: ^1.0.2
nanoid: 5.1.6
node-abi: ^4.31.0
node-pty: ^1.1.0
ofetch: ^1.5.1
oxc-minify: ^0.126.0
@ -135,6 +136,7 @@ catalog:
tinyexec: ^1.1.1
tsdown: ^0.21.9
tsx: ^4.21.0
uiohook-napi: ^1.5.5
uncrypto: ^0.1.3
unplugin-info: ^1.3.2
unstorage: ^1.17.5
@ -194,6 +196,7 @@ onlyBuiltDependencies:
- sharp
- spawn-sync
- stockfish
- uiohook-napi
- utf-8-validate
- vue-demi
packageExtensions: