feat(stage-*): experimental realtime transcription powered by Aliyun NLS

This commit is contained in:
Neko Ayaka
2025-11-02 04:20:27 +08:00
parent 7bde379ab3
commit 69b8a56937
16 changed files with 1101 additions and 21 deletions
+1
View File
@@ -93,6 +93,7 @@
"three": "^0.181.0",
"unified": "^11.0.5",
"unspeech": "^0.1.7",
"uuid": "^13.0.0",
"valibot": "1.0.0-beta.9",
"vaul-vue": "^0.4.1",
"vue": "^3.5.22",
@@ -13,7 +13,7 @@ import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consci
import { useHearingSpeechInputPipeline } from '@proj-airi/stage-ui/stores/modules/hearing'
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
import { useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings'
import { debouncedRef, useBroadcastChannel, watchPausable } from '@vueuse/core'
import { refDebounced, useBroadcastChannel } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { computed, onUnmounted, ref, toRef, watch } from 'vue'
@@ -43,11 +43,11 @@ const shouldFadeOnCursorWithin = ref(false)
const { isOutside: isOutsideWindow } = useElectronMouseInWindow()
const { isOutside } = useElectronMouseInElement(controlsIslandRef)
const isOutsideFor250Ms = debouncedRef(isOutside, 250)
const isOutsideFor250Ms = refDebounced(isOutside, 250)
const { x: relativeMouseX, y: relativeMouseY } = useElectronRelativeMouse()
const isTransparent = useCanvasPixelIsTransparentAtPoint(stageCanvas, relativeMouseX, relativeMouseY)
const { isNearAnyBorder: isAroundWindowBorder } = useElectronMouseAroundWindowBorder({ threshold: 30 })
const isAroundWindowBorderFor250Ms = debouncedRef(isAroundWindowBorder, 250)
const isAroundWindowBorderFor250Ms = refDebounced(isAroundWindowBorder, 250)
const setIgnoreMouseEvents = useElectronEventaInvoke(electron.window.setIgnoreMouseEvents)
@@ -56,7 +56,7 @@ const { live2dLookAtX, live2dLookAtY } = storeToRefs(useWindowStore())
watch(componentStateStage, () => isLoading.value = componentStateStage.value !== 'mounted', { immediate: true })
const { pause, resume } = watchPausable(isTransparent, (transparent) => {
const { pause, resume } = watch(isTransparent, (transparent) => {
shouldFadeOnCursorWithin.value = !transparent
}, { immediate: true })
@@ -36,6 +36,12 @@ const menu = computed(() => [
icon: 'i-solar:sledgehammer-bold-duotone',
to: '/devtools/use-electron-relative-mouse',
},
{
title: 'Aliyun Real-time Transcriber',
description: 'Stream microphone audio to Aliyun NLS and inspect live transcripts',
icon: 'i-solar:sledgehammer-bold-duotone',
to: '/devtools/providers-transcription-realtime-aliyun-nls',
},
])
const { context } = createContext(window.electron.ipcRenderer)
+1
View File
@@ -80,6 +80,7 @@
"three": "^0.181.0",
"unified": "^11.0.5",
"unspeech": "^0.1.7",
"uuid": "^13.0.0",
"valibot": "1.0.0-beta.9",
"vaul-vue": "^0.4.1",
"vue": "^3.5.22",
@@ -62,6 +62,12 @@ const menu = computed(() => [
icon: 'i-solar:sledgehammer-bold-duotone',
to: '/devtools/vibrant',
},
{
title: 'Aliyun Real-time Transcriber',
description: 'Stream microphone audio to Aliyun NLS and inspect live transcripts',
icon: 'i-solar:sledgehammer-bold-duotone',
to: '/devtools/providers-transcription-realtime-aliyun-nls',
},
])
</script>
+2
View File
@@ -11,6 +11,7 @@ words:
- alexanderolsen
- alibabacloud
- aliyun
- aliyuncs
- allseto
- animejs
- APNG
@@ -238,6 +239,7 @@ words:
- sensenova
- serde
- Shadcn
- shenzhen
- shiki
- shikijs
- silero
@@ -0,0 +1,542 @@
<script setup lang="ts">
import type { ServerEvent, ServerEvents } from '@proj-airi/stage-ui/stores/providers/aliyun'
import vadWorkletUrl from '@proj-airi/stage-ui/workers/vad/process.worklet?worker&url'
import { Button } from '@proj-airi/stage-ui/components'
import { createAliyunNLSSession } from '@proj-airi/stage-ui/stores/providers/aliyun'
import { FieldInput, FieldSelect } from '@proj-airi/ui'
import { computed, nextTick, onBeforeUnmount, reactive, ref, shallowRef, watch } from 'vue'
type ConnectionState = 'idle' | 'connecting' | 'connected' | 'error' | 'closed'
type AliyunRegion
= | 'cn-shanghai'
| 'cn-shanghai-internal'
| 'cn-beijing'
| 'cn-beijing-internal'
| 'cn-shenzhen'
| 'cn-shenzhen-internal'
const credentials = reactive({
accessKeyId: '',
accessKeySecret: '',
appKey: '',
region: 'cn-shanghai' as AliyunRegion,
})
const connectionState = ref<ConnectionState>('idle')
const transcriptionReady = ref(false)
const isRecording = ref(false)
const currentPartial = ref<string | undefined>('')
const transcripts = ref<Array<{ index: number, text: string, final: boolean }>>([])
const websocket = shallowRef<WebSocket>()
const session = shallowRef<ReturnType<typeof createAliyunNLSSession>>()
const sessionId = ref('')
const audioContext = shallowRef<AudioContext>()
const workletNode = shallowRef<AudioWorkletNode>()
const mediaStreamSource = shallowRef<MediaStreamAudioSourceNode>()
const mediaStream = shallowRef<MediaStream>()
const logs = ref<Array<{ id: number, level: 'info' | 'error', text: string }>>([])
const logsContainer = ref<HTMLDivElement>()
const regionOptions: { label: string, value: AliyunRegion }[] = [
{ label: 'cn-shanghai', value: 'cn-shanghai' },
{ label: 'cn-beijing', value: 'cn-beijing' },
{ label: 'cn-shenzhen', value: 'cn-shenzhen' },
{ label: 'cn-shanghai (internal)', value: 'cn-shanghai-internal' },
{ label: 'cn-beijing (internal)', value: 'cn-beijing-internal' },
{ label: 'cn-shenzhen (internal)', value: 'cn-shenzhen-internal' },
]
const statusLabel = computed(() => {
switch (connectionState.value) {
case 'connecting':
return 'Connecting'
case 'connected':
return 'Connected'
case 'error':
return 'Error'
case 'closed':
return 'Disconnected'
default:
return 'Idle'
}
})
const statusColor = computed(() => {
switch (connectionState.value) {
case 'connected':
return 'text-green-500'
case 'connecting':
return 'text-blue-500'
case 'error':
return 'text-red-500'
default:
return 'text-neutral-500 dark:text-neutral-400'
}
})
const canConnect = computed(() => {
return (
!!credentials.accessKeyId
&& !!credentials.accessKeySecret
&& !!credentials.appKey
&& connectionState.value !== 'connecting'
&& connectionState.value !== 'connected'
)
})
const canStartRecording = computed(() => {
return connectionState.value === 'connected' && transcriptionReady.value && !isRecording.value
})
const canStopRecording = computed(() => isRecording.value)
const canDisconnect = computed(() => websocket.value && websocket.value.readyState !== WebSocket.CLOSED)
let audioChunkCount = 0
let lastChunkLogAt = 0
watch(logs, () => {
nextTick(() => {
const container = logsContainer.value
if (container)
container.scrollTop = container.scrollHeight
})
})
function appendLog(message: string, level: 'info' | 'error' = 'info') {
logs.value.push({
id: Date.now() + Math.random(),
level,
text: `[${new Date().toLocaleTimeString()}] ${message}`,
})
}
function float32ToInt16(buffer: Float32Array) {
const output = new Int16Array(buffer.length)
for (let i = 0; i < buffer.length; i++) {
const value = Math.max(-1, Math.min(1, buffer[i]))
output[i] = value < 0 ? value * 0x8000 : value * 0x7FFF
}
return output
}
function ensureWebSocketOpen() {
return websocket.value && websocket.value.readyState === WebSocket.OPEN
}
function resetTranscriptionState() {
transcriptionReady.value = false
currentPartial.value = ''
transcripts.value = []
audioChunkCount = 0
lastChunkLogAt = 0
}
async function initializeAudioGraph(stream: MediaStream) {
const context = new AudioContext({
sampleRate: 16000,
latencyHint: 'interactive',
})
await context.audioWorklet.addModule(vadWorkletUrl)
const node = new AudioWorkletNode(context, 'vad-audio-worklet-processor')
node.port.onmessage = ({ data }: MessageEvent<{ buffer?: Float32Array }>) => {
const buffer = data.buffer
if (!buffer || !ensureWebSocketOpen())
return
audioChunkCount += 1
if (audioChunkCount === 1 || audioChunkCount - lastChunkLogAt >= 50) {
appendLog(`Streaming audio chunk #${audioChunkCount}`)
lastChunkLogAt = audioChunkCount
}
const pcm16 = float32ToInt16(buffer)
websocket.value?.send(pcm16.buffer)
}
const source = context.createMediaStreamSource(stream)
source.connect(node)
const silentGain = context.createGain()
silentGain.gain.value = 0
node.connect(silentGain)
silentGain.connect(context.destination)
audioContext.value = context
workletNode.value = node
mediaStreamSource.value = source
}
async function startRecording() {
if (!ensureWebSocketOpen()) {
appendLog('WebSocket is not ready. Connect before starting recording.', 'error')
return
}
if (isRecording.value)
return
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
channelCount: 1,
sampleRate: 16000,
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
},
})
mediaStream.value = stream
await initializeAudioGraph(stream)
if (audioContext.value?.state === 'suspended')
await audioContext.value.resume()
isRecording.value = true
appendLog('Recording started')
}
catch (error) {
appendLog(`Failed to start recording: ${error instanceof Error ? error.message : String(error)}`, 'error')
await stopRecording()
}
}
async function stopRecording() {
if (!isRecording.value)
return
try {
workletNode.value?.port.postMessage({ type: 'stop' })
}
catch { /* ignore */ }
if (mediaStreamSource.value) {
mediaStreamSource.value.disconnect()
mediaStreamSource.value = undefined
}
if (workletNode.value) {
workletNode.value.port.onmessage = null
workletNode.value.disconnect()
workletNode.value = undefined
}
if (mediaStream.value) {
mediaStream.value.getTracks().forEach(track => track.stop())
mediaStream.value = undefined
}
if (audioContext.value) {
try {
await audioContext.value.close()
}
catch (error) {
console.error('Failed to close audio context', error)
}
audioContext.value = undefined
}
isRecording.value = false
appendLog('Recording stopped')
}
function handleServerEvent(event: ServerEvent) {
switch (event.header.name) {
case 'TranscriptionStarted':
{
const payload = event.payload as ServerEvents['TranscriptionStarted']
transcriptionReady.value = true
appendLog(`Transcription started. Session: ${payload.session_id}`)
break
}
case 'TranscriptionResultChanged':
{
const payload = event.payload as ServerEvents['TranscriptionResultChanged']
currentPartial.value = payload.result
upsertTranscript(payload.index, payload.result, false)
break
}
case 'SentenceEnd':
{
const payload = event.payload as ServerEvents['SentenceEnd']
currentPartial.value = ''
upsertTranscript(payload.index, payload.result, true)
appendLog(`Sentence #${payload.index} (${payload.time}ms): ${payload.result}`)
break
}
case 'TranscriptionCompleted':
appendLog('Transcription completed')
break
default:
appendLog(`Server event: ${event.header.name}`)
break
}
}
function upsertTranscript(index: number, text: string, final: boolean) {
const existingIndex = transcripts.value.findIndex(entry => entry.index === index)
if (existingIndex >= 0) {
const existing = transcripts.value[existingIndex]
transcripts.value.splice(existingIndex, 1, {
index,
text,
final: existing.final || final,
})
}
else {
transcripts.value.push({ index, text, final })
}
transcripts.value.sort((a, b) => a.index - b.index)
}
async function connectWebSocket() {
if (!canConnect.value)
return
resetTranscriptionState()
connectionState.value = 'connecting'
try {
session.value = createAliyunNLSSession(
credentials.accessKeyId.trim(),
credentials.accessKeySecret.trim(),
credentials.appKey.trim(),
{ region: credentials.region },
)
sessionId.value = session.value.sessionId
const url = await session.value.websocketUrl()
appendLog(`Connecting to ${url}`)
const ws = new WebSocket(url)
ws.binaryType = 'arraybuffer'
ws.onopen = () => {
connectionState.value = 'connected'
appendLog('WebSocket connected')
session.value?.start(ws, { enable_intermediate_result: true, enable_punctuation_prediction: true })
}
ws.onerror = (event) => {
connectionState.value = 'error'
appendLog(`WebSocket error: ${JSON.stringify(event)}`, 'error')
}
ws.onmessage = (messageEvent) => {
try {
const data = JSON.parse(messageEvent.data)
session.value?.onEvent(data, handleServerEvent)
}
catch {
appendLog(`Server message: ${messageEvent.data}`)
}
}
ws.onclose = () => {
connectionState.value = 'closed'
appendLog('WebSocket closed by server')
cleanupConnection()
}
websocket.value = ws
}
catch (error) {
connectionState.value = 'error'
appendLog(`Failed to connect: ${error instanceof Error ? error.message : String(error)}`, 'error')
cleanupConnection()
}
}
async function disconnectWebSocket() {
await stopRecording()
if (websocket.value && websocket.value.readyState === WebSocket.OPEN) {
try {
session.value?.stop(websocket.value)
}
catch (error) {
appendLog(`Failed to send stop event: ${error instanceof Error ? error.message : String(error)}`, 'error')
}
websocket.value.close()
}
else {
cleanupConnection()
connectionState.value = 'closed'
}
}
function cleanupConnection() {
stopRecording()
websocket.value = undefined
session.value = undefined
resetTranscriptionState()
}
onBeforeUnmount(async () => {
await disconnectWebSocket()
})
</script>
<template>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-semibold">
Aliyun NLS Realtime Transcription
</h1>
<p class="mt-1 text-sm text-neutral-500 dark:text-neutral-400">
Access Key ID and Secret with SpeechTranscriber permissions are required.
</p>
</div>
<section class="space-y-4">
<div class="grid gap-4 md:grid-cols-2">
<FieldInput
v-model="credentials.accessKeyId"
label="Access Key ID"
description="RAM AccessKey ID with SpeechTranscriber permissions."
placeholder="LTAI..."
/>
<FieldInput
v-model="credentials.accessKeySecret"
label="Access Key Secret"
description="Keep this secret safe; it never leaves this page."
placeholder="****************"
type="password"
/>
<FieldInput
v-model="credentials.appKey"
label="App Key"
description="NLS project AppKey to bind the transcription session."
placeholder="请输入 AppKey"
/>
<FieldSelect
v-model="credentials.region"
label="Region"
description="Match the region used when issuing the token."
:options="regionOptions"
placeholder="cn-shanghai"
layout="vertical"
/>
</div>
<div class="flex flex-wrap items-center gap-4">
<div class="text-sm">
<span class="text-neutral-500 dark:text-neutral-400">Status:</span>
<span class="ml-2 font-medium" :class="statusColor">
{{ statusLabel }}
</span>
<span v-if="isRecording" class="ml-2 rounded bg-red-500/10 px-2 py-0.5 text-xs text-red-500">
Recording
</span>
</div>
<div class="flex flex-wrap gap-3">
<Button
:disabled="!canConnect"
variant="primary"
@click="connectWebSocket"
>
Connect
</Button>
<Button
:disabled="!canStartRecording"
variant="primary"
@click="startRecording"
>
Listen
</Button>
<Button
:disabled="!canStopRecording"
variant="danger"
@click="stopRecording"
>
Stop
</Button>
<Button
:disabled="!canDisconnect"
variant="secondary"
@click="disconnectWebSocket"
>
Disconnect
</Button>
</div>
</div>
</section>
<section class="space-y-3">
<h2 class="text-lg font-semibold">
Transcripts
</h2>
<div class="border border-neutral-200/80 rounded bg-neutral-50/60 p-4 text-sm dark:border-neutral-700 dark:bg-neutral-900/50">
<div v-if="currentPartial" class="mb-3 text-neutral-500 dark:text-neutral-400">
<div class="text-xs text-neutral-400 tracking-wide uppercase dark:text-neutral-500">
Partial
</div>
<div class="mt-1 font-medium">
{{ currentPartial }}
</div>
</div>
<div v-if="!transcripts.length && !currentPartial" class="text-neutral-400 dark:text-neutral-600">
Waiting for server...
</div>
<ul class="space-y-2">
<li
v-for="sentence in transcripts"
:key="sentence.index"
class="flex items-start gap-2"
>
<span class="mt-0.5 rounded bg-neutral-200/80 px-2 py-0.5 text-xs text-neutral-700 dark:bg-neutral-800/70 dark:text-neutral-200">
#{{ sentence.index }}
</span>
<div>
<div class="font-medium" :class="sentence.final ? '' : 'italic text-neutral-500 dark:text-neutral-400'">
{{ sentence.text }}
</div>
<div v-if="!sentence.final" class="text-xs text-neutral-400">
Waiting for final result...
</div>
</div>
</li>
</ul>
</div>
</section>
<section class="space-y-3">
<h2 class="text-lg font-semibold">
Logs
</h2>
<div
ref="logsContainer"
class="h-64 overflow-y-auto border border-neutral-200/80 rounded bg-neutral-50/60 p-3 text-xs leading-5 dark:border-neutral-700 dark:bg-neutral-900/50"
>
<div
v-for="entry in logs"
:key="entry.id"
:class="entry.level === 'error' ? 'text-red-500' : 'text-neutral-700 dark:text-neutral-200'"
>
{{ entry.text }}
</div>
</div>
</section>
</div>
</template>
<route lang="yaml">
meta:
layout: settings
</route>
+2
View File
@@ -0,0 +1,2 @@
ALIYUN_AK_ID=
ALIYUN_AK_SECRET=
+3
View File
@@ -26,6 +26,7 @@
"./constants": "./src/constants/index.ts",
"./libs/*": "./src/libs/*.ts",
"./libs": "./src/libs/index.ts",
"./stores/providers/aliyun": "./src/stores/providers/aliyun/index.ts",
"./stores/*": "./src/stores/*.ts",
"./stores": "./src/stores/index.ts",
"./workers/vad": "./src/workers/vad/index.ts",
@@ -108,6 +109,7 @@
"localforage": "^1.10.0",
"mediabunny": "^1.24.2",
"nanoid": "^5.1.6",
"ofetch": "^1.5.0",
"pinia": "^3.0.3",
"pixi-filters": "4",
"pixi-live2d-display": "^0.4.0",
@@ -123,6 +125,7 @@
"unified": "^11.0.5",
"unist-builder": "^4.0.0",
"unspeech": "^0.1.7",
"uuid": "^13.0.0",
"vaul-vue": "^0.4.1",
"vue-i18n": "^11.1.12",
"vue-router": "^4.6.3",
@@ -0,0 +1,255 @@
/**
* https://help.aliyun.com/zh/isi/developer-reference/websocket?spm=a2c4g.11186623.help-menu-30413.d_3_2_1_8.694e8d64PgomCT
*/
import { utc } from '@date-fns/utc'
import { merge } from '@moeru/std'
import { isBefore } from 'date-fns'
import { customAlphabet } from 'nanoid'
import { createToken } from './token'
import { nlsWebSocketEndpointFromRegion } from './utils'
// NOTICE: Aliyun NLS requires exact 32 character length in hex for IDs,
// so we use a custom nanoid alphabet and length here.
const nanoid = customAlphabet('0123456789abcdef', 32)
interface BaseEventHeader<N> {
appkey: string
message_id: string
task_id: string
namespace: 'SpeechTranscriber'
name: N
status?: 20000000
status_message?: 'GATEWAY' | 'SUCCESS' | 'Success'
}
interface BaseEvent<N, P> {
header: BaseEventHeader<N>
payload: P
}
export interface EventStartTranscription extends BaseEvent<'StartTranscription', {
/** Audio format, supports PCM, WAV, OPUS, SPEEX, AMR, MP3, AAC. */
format?: 'pcm' | 'wav' | 'opus' | 'speex' | 'amr' | 'mp3' | 'aac'
/** Audio sample rate in Hz. Default is 16000. Make sure the project is configured for the selected rate. */
sample_rate?: 8000 | 16000
/** Return interim recognition results. Disabled by default. */
enable_intermediate_result?: boolean
/** Add punctuation during post-processing. Disabled by default. */
enable_punctuation_prediction?: boolean
/** Inverse text normalization (ITN). Converts Chinese numerals to Arabic digits when true. Disabled by default. */
enable_inverse_text_normalization?: boolean
/** Custom model ID. */
customization_id?: string
/** Custom vocabulary ID. */
vocabulary_id?: string
/** Silence threshold in milliseconds for sentence segmentation. Range 2002000 ms, default 800 ms. */
max_sentence_silence?: number
/** Return word-level timestamps. Disabled by default. */
enable_words?: boolean
/** Filter filler words (disfluency). Requires version 4.0. Disabled by default. */
disfluency?: boolean
/**
* Noise threshold. Range [-1, 1].
* Closer to -1 treats noise as speech more aggressively.
* Closer to +1 treats speech as noise more aggressively.
* Important: this is an advanced parameter and should be tuned carefully with dedicated testing.
*/
speech_noise_threshold?: number
/** Enable semantic sentence segmentation. Disabled by default. */
enable_semantic_sentence_detection?: boolean
}> {}
export interface EventStopTranscription extends BaseEvent<'StopTranscription', undefined> {}
export interface EventTranscriptionStarted extends BaseEvent<'TranscriptionStarted', {
/** Session ID. Returned as-is if provided by the client, otherwise generated by the server. */
session_id: string
}> {}
export interface EventSentenceBegin extends BaseEvent<'SentenceBegin', {
/** Incremental sentence index starting from 1. */
index: number
/** Sentence start time relative to the audio stream start, in milliseconds. */
time: number
}> {}
export interface EventTranscriptionResultChanged extends BaseEvent<'TranscriptionResultChanged', {
/** Incremental sentence index starting from 1. */
index: number
/** Audio duration processed so far, in milliseconds. */
time: number
/** Current recognition text. */
result: string
/** Word-level alignment information. */
words?: {
text: string
startTime: number
endTime: number
}[]
/** Status code returned by the service. */
status: number
}> {}
export interface EventSentenceEnd extends BaseEvent<'SentenceEnd', {
/** Incremental sentence index starting from 1. */
index: number
/** Audio duration processed so far, in milliseconds. */
time: number
/** Start time of the matching `SentenceBegin` event, in milliseconds. */
begin_time: number
/** Final recognition text for the sentence. */
result: string
/** Confidence score in the range [0.0, 1.0]. */
confidence: number
/** Word-level alignment information. */
words?: {
/** Word text */
text: string
/** Word start time */
startTime: number
/** Word end time */
endTime: number
}[]
/** Status code returned by the service. */
status: number
/** Buffered result when semantic segmentation is enabled; contains the upcoming unfinished sentence. */
stash_result: {
/** Incremental sentence index starting from 1. */
sentenceId: number
/** Sentence start time. */
beginTime: number
/** Recognized text. */
text: string
/** Current processing time. */
currentTime: number
}
}> {}
export interface EventTranscriptionCompleted extends BaseEvent<'TranscriptionCompleted', undefined> {}
export interface ClientEvents {
StartTranscription: EventStartTranscription['payload']
StopTranscription: EventStopTranscription['payload']
}
export type ClientEvent = {
[K in keyof ClientEvents]: BaseEvent<K, ClientEvents[K]>;
}[keyof ClientEvents]
export interface ServerEvents {
TranscriptionStarted: EventTranscriptionStarted['payload']
SentenceBegin: EventSentenceBegin['payload']
TranscriptionResultChanged: EventTranscriptionResultChanged['payload']
SentenceEnd: EventSentenceEnd['payload']
TranscriptionCompleted: EventTranscriptionCompleted['payload']
}
export type ServerEvent = {
[K in keyof ServerEvents]: BaseEvent<K, ServerEvents[K]>;
}[keyof ServerEvents]
export function createAliyunNLSSession(
accessKeyId: string,
accessKeySecret: string,
appKey: string,
options?: {
region?:
| 'cn-shanghai'
| 'cn-shanghai-internal'
| 'cn-beijing'
| 'cn-beijing-internal'
| 'cn-shenzhen'
| 'cn-shenzhen-internal'
},
) {
const provider = createAliyunNLSProvider(accessKeyId, accessKeySecret, appKey, options)
const providerSessionId = nanoid()
function start(websocketConn: WebSocket, options?: {
sessionId?: string
} & EventStartTranscription['payload']) {
const mergedOptions = merge({ sessionId: providerSessionId }, options)
websocketConn.send(JSON.stringify({
header: {
appkey: provider.appKey,
message_id: nanoid(),
task_id: mergedOptions.sessionId,
namespace: 'SpeechTranscriber',
name: 'StartTranscription',
},
payload: {
format: 'wav',
},
} satisfies EventStartTranscription))
}
function stop(websocketConn: WebSocket, options?: {
sessionId?: string
}) {
const mergedOptions = merge({ sessionId: providerSessionId }, options)
websocketConn.send(JSON.stringify({
header: {
appkey: provider.appKey,
message_id: nanoid(),
task_id: mergedOptions.sessionId,
namespace: 'SpeechTranscriber',
name: 'StopTranscription',
},
payload: undefined,
} satisfies EventStopTranscription))
}
function onEvent(data: unknown, cb: (event: ServerEvent) => void) {
const event = data as ServerEvent
cb(event)
}
return {
...provider,
sessionId: providerSessionId,
start,
stop,
onEvent,
}
}
export function createAliyunNLSProvider(
accessKeyId: string,
accessKeySecret: string,
appKey: string,
options?: {
region?:
| 'cn-shanghai'
| 'cn-shanghai-internal'
| 'cn-beijing'
| 'cn-beijing-internal'
| 'cn-shenzhen'
| 'cn-shenzhen-internal'
},
) {
let token: string = ''
let tokenExpiresAt: number = utc(new Date()).getTime()
async function websocketUrl() {
if (!token || isBefore(new Date(tokenExpiresAt), utc(new Date()))) {
const created = await createToken(accessKeyId, accessKeySecret, { regionId: options?.region ?? 'cn-shanghai' })
token = created.token
tokenExpiresAt = created.expiresAt
}
const url = nlsWebSocketEndpointFromRegion(options?.region ?? 'cn-shanghai')
url.searchParams.set('token', token)
return url.toString()
}
return {
websocketUrl,
appKey,
}
}
@@ -0,0 +1,85 @@
import { env } from 'node:process'
import { utc } from '@date-fns/utc'
import { isAfter, parse } from 'date-fns'
import { describe, expect, it } from 'vitest'
import {
buildCreateTokenRequest,
canonicalizeQuery,
createStringToSign,
createToken,
signStringToBase64,
} from './token'
describe('buildCreateTokenRequest', () => {
const testParameters = {
AccessKeyId: 'my_access_key_id',
Action: 'CreateToken',
Format: 'JSON',
RegionId: 'cn-shanghai',
SignatureMethod: 'HMAC-SHA1',
SignatureNonce: 'b924c8c3-6d03-4c5d-ad36-d984d3116788',
SignatureVersion: '1.0',
Timestamp: '2019-04-18T08:32:31Z',
Version: '2019-02-28',
}
const expectedCanonicalQuery = 'AccessKeyId=my_access_key_id&Action=CreateToken&Format=JSON&RegionId=cn-shanghai&SignatureMethod=HMAC-SHA1&SignatureNonce=b924c8c3-6d03-4c5d-ad36-d984d3116788&SignatureVersion=1.0&Timestamp=2019-04-18T08%3A32%3A31Z&Version=2019-02-28'
const expectedBuiltQueryString = 'GET&%2F&AccessKeyId%3Dmy_access_key_id%26Action%3DCreateToken%26Format%3DJSON%26RegionId%3Dcn-shanghai%26SignatureMethod%3DHMAC-SHA1%26SignatureNonce%3Db924c8c3-6d03-4c5d-ad36-d984d3116788%26SignatureVersion%3D1.0%26Timestamp%3D2019-04-18T08%253A32%253A31Z%26Version%3D2019-02-28'
const expectedSignature = 'hHq4yNsPitlfDJ2L0nQPdugdEzM='
const expectedSignatureEncoded = encodeURIComponent(expectedSignature)
const expectedSignedQuery = `Signature=${expectedSignatureEncoded}&${expectedCanonicalQuery}`
const expectedUrl = `http://nls-meta.cn-shanghai.aliyuncs.com/?${expectedSignedQuery}`
it('builds canonical query string matching the Java implementation', () => {
const canonical = canonicalizeQuery(testParameters)
expect(canonical).toBe(expectedCanonicalQuery)
})
it('creates the expected string to sign', () => {
const canonical = canonicalizeQuery(testParameters)
const stringToSign = createStringToSign('GET', '/', canonical)
expect(stringToSign).toBe(expectedBuiltQueryString)
})
it('produces the expected signature', async () => {
const signature = await signStringToBase64(expectedBuiltQueryString, 'my_access_key_secret')
expect(signature).toBe(expectedSignature)
expect(encodeURIComponent(signature)).toBe(expectedSignatureEncoded)
})
it('constructs the full token request data', async () => {
const request = await buildCreateTokenRequest(
'my_access_key_id',
'my_access_key_secret',
{
timestamp: parse(testParameters.Timestamp, 'yyyy-MM-dd\'T\'HH:mm:ssX', new Date()),
signatureNonce: testParameters.SignatureNonce,
},
)
expect(request.canonicalQuery).toBe(expectedCanonicalQuery)
expect(request.stringToSign).toBe(expectedBuiltQueryString)
expect(request.signature).toBe(expectedSignature)
expect(request.encodedSignature).toBe(expectedSignatureEncoded)
expect(request.signedQuery).toBe(expectedSignedQuery)
expect(request.url).toBe(expectedUrl)
expect(request.params.Signature).toBe(expectedSignature)
})
})
describe('createToken', (test) => {
it('successfully fetches a token', async () => {
if (!env.ALIYUN_AK_ID || !env.ALIYUN_AK_SECRET) {
test.skip('ALIYUN_AK_ID and ALIYUN_AK_SECRET must be set in environment to run this test', () => {})
return
}
const { token, expiresAt } = await createToken(env.ALIYUN_AK_ID!, env.ALIYUN_AK_SECRET!)
expect(token).toBeDefined()
expect(token).toBeTypeOf('string')
expect(expiresAt).toBeTypeOf('number')
expect(isAfter(new Date(expiresAt), utc(new Date()))).toBe(true)
})
})
@@ -0,0 +1,134 @@
import { utc } from '@date-fns/utc'
import { encodeBase64, merge } from '@moeru/std'
import { format } from 'date-fns'
import { ofetch } from 'ofetch'
import { subtle } from 'uncrypto'
import { v4 as uuidV4 } from 'uuid'
import { nlsMetaEndpointFromRegion } from './utils'
const SIGNING_METHOD = 'HMAC-SHA1'
const SIGNATURE_VERSION = '1.0'
const API_VERSION = '2019-02-28'
type AliyunQueryParams = Record<string, string>
export interface CreateTokenOptions {
regionId?: string
endpoint?: string
timestamp?: Date
signatureNonce?: string
extraQuery?: AliyunQueryParams
}
export interface CreateTokenRequest {
endpoint: string
canonicalQuery: string
stringToSign: string
signature: string
encodedSignature: string
signedQuery: string
params: AliyunQueryParams
url: string
}
export function canonicalizeQuery(params: AliyunQueryParams): string {
return Object.keys(params)
.sort()
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
.join('&')
}
export function createStringToSign(
method: string,
path: string,
canonicalQuery: string,
): string {
const encodedPath = encodeURIComponent(path)
const encodedQuery = encodeURIComponent(canonicalQuery)
return `${method}&${encodedPath}&${encodedQuery}`
}
export async function signStringToBase64(stringToSign: string, accessKeySecret: string): Promise<string> {
const encoder = new TextEncoder()
const keyData = encoder.encode(`${accessKeySecret}&`)
const algorithm: HmacImportParams = {
name: 'HMAC',
hash: { name: 'SHA-1' },
}
const cryptoKey = await subtle.importKey(
'raw',
keyData as Uint8Array<ArrayBuffer>,
algorithm,
false,
['sign'],
)
const signDataEncoder = new TextEncoder()
const data = signDataEncoder.encode(stringToSign)
const signatureBuffer = await subtle.sign('HMAC', cryptoKey, data as Uint8Array<ArrayBuffer>)
return encodeBase64(signatureBuffer)
}
export async function buildCreateTokenRequest(accessKeyId: string, accessKeySecret: string, options?: CreateTokenOptions): Promise<CreateTokenRequest> {
const mergedOptions = merge({ timestamp: new Date() }, options)
// ISO 8601 format: YYYY-MM-DDThh:mm:ssZ
const timestamp = format(utc(mergedOptions.timestamp), 'yyyy-MM-dd\'T\'HH:mm:ssXX')
const signatureNonce = options?.signatureNonce ?? uuidV4()
const params: AliyunQueryParams = {
AccessKeyId: accessKeyId,
Action: 'CreateToken',
Format: 'JSON',
RegionId: options?.regionId ?? 'cn-shanghai',
SignatureMethod: SIGNING_METHOD,
SignatureNonce: signatureNonce,
SignatureVersion: SIGNATURE_VERSION,
Timestamp: timestamp,
Version: API_VERSION,
...options?.extraQuery,
}
const canonicalQuery = canonicalizeQuery(params)
const stringToSign = createStringToSign('POST', '/', canonicalQuery)
const signatureBase64 = await signStringToBase64(stringToSign, accessKeySecret)
const encodedSignature = encodeURIComponent(signatureBase64)
const signedQuery = `Signature=${encodedSignature}&${canonicalQuery}`
const endpoint = (options?.endpoint ?? nlsMetaEndpointFromRegion(options?.regionId ?? 'cn-shanghai').toString()).replace(/\/$/, '')
const url = `${endpoint}/?${signedQuery}`
return {
endpoint,
canonicalQuery,
stringToSign,
signature: signatureBase64,
encodedSignature,
signedQuery,
params: {
Signature: signatureBase64,
...params,
},
url,
}
}
export async function createToken(accessKeyId: string, accessKeySecret: string, options?: CreateTokenOptions): Promise<{ token: string, expiresAt: number }> {
const request = await buildCreateTokenRequest(accessKeyId, accessKeySecret, options)
const response = await ofetch<{
NlsRequestId: string
RequestId: string
ErrMsg: string
Token: { ExpireTime: number, Id: string, UserId: string }
} | {
RequestId: string
Message: string
Code: string
}>(request.url, { method: 'POST' })
if ('Token' in response && typeof response.Token === 'object' && 'Id' in response.Token) {
return { token: response.Token.Id, expiresAt: response.Token.ExpireTime * 1000 }
}
throw new Error(`Failed to create token: ${JSON.stringify(response) || 'Unknown error'}`)
}
@@ -0,0 +1,23 @@
export function nlsMetaEndpointFromRegion(region: string): URL {
return new URL(`http://nls-meta.${region}.aliyuncs.com`)
}
export function nlsWebSocketEndpointFromRegion(region: string = 'cn-shanghai'): URL {
const websocketURL = new URL('/ws/v1', 'https://example.com')
switch (region) {
case 'cn-shanghai':
case 'cn-beijing':
case 'cn-shenzhen':
websocketURL.protocol = 'wss:'
websocketURL.hostname = `nls-gateway-${region}.aliyuncs.com`
break
case 'cn-shanghai-internal':
case 'cn-beijing-internal':
case 'cn-shenzhen-internal':
websocketURL.protocol = 'wss:'
websocketURL.hostname = `nls-gateway-${region}-internal.aliyuncs.com:80`
}
return websocketURL
}
+11 -4
View File
@@ -1,7 +1,14 @@
import { join } from 'node:path'
import { cwd } from 'node:process'
import { loadEnv } from 'vite'
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: ['src/**/*.test.ts'],
},
export default defineConfig(({ mode }) => {
return ({
test: {
include: ['src/**/*.test.ts'],
env: loadEnv(mode, join(cwd(), 'packages', 'stage-ui'), ''),
},
})
})
+25 -13
View File
@@ -461,6 +461,9 @@ importers:
unspeech:
specifier: ^0.1.7
version: 0.1.7
uuid:
specifier: ^13.0.0
version: 13.0.0
valibot:
specifier: 1.0.0-beta.9
version: 1.0.0-beta.9(typescript@5.9.3)
@@ -843,6 +846,9 @@ importers:
unspeech:
specifier: ^0.1.7
version: 0.1.7
uuid:
specifier: ^13.0.0
version: 13.0.0
valibot:
specifier: 1.0.0-beta.9
version: 1.0.0-beta.9(typescript@5.9.3)
@@ -1044,7 +1050,7 @@ importers:
version: 11.1.12(vue@3.5.22(typescript@5.9.3))
vue-sonner:
specifier: ^2.0.9
version: 2.0.9(@nuxt/kit@4.0.3(magicast@0.3.5))(@nuxt/schema@4.0.3)(nuxt@4.0.3(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.0)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(bufferutil@4.0.9)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7))(eslint@9.39.0(jiti@2.5.1))(ioredis@5.7.0)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@7.1.12(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))(vue-tsc@3.1.2(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.1))
version: 2.0.9(@nuxt/kit@4.0.3(magicast@0.3.5))(@nuxt/schema@4.0.3)(nuxt@4.0.3(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.0)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(bufferutil@4.0.9)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7))(encoding@0.1.13)(eslint@9.39.0(jiti@2.5.1))(ioredis@5.7.0)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@7.1.12(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))(vue-tsc@3.1.2(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.1))
devDependencies:
'@iconify-json/lucide':
specifier: ^1.2.71
@@ -1123,7 +1129,7 @@ importers:
version: 0.1.3
unplugin-yaml:
specifier: ^3.0.7
version: 3.0.7(@nuxt/kit@4.0.3(magicast@0.3.5))(@nuxt/schema@4.0.3)(astro@5.10.1(@netlify/blobs@9.1.2)(@types/node@24.9.2)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(ioredis@5.7.0)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(esbuild@0.25.9)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(vite@7.1.12(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))
version: 3.0.7(@nuxt/kit@4.0.3(magicast@0.3.5))(@nuxt/schema@4.0.3)(astro@5.10.1(@netlify/blobs@9.1.2)(@types/node@24.9.2)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(encoding@0.1.13)(ioredis@5.7.0)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(esbuild@0.25.9)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(vite@7.1.12(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))
vitepress:
specifier: ^2.0.0-alpha.12
version: 2.0.0-alpha.12(@types/node@24.9.2)(change-case@5.4.4)(fuse.js@7.1.0)(jiti@2.5.1)(jwt-decode@4.0.0)(less@4.4.2)(lightningcss@1.30.2)(nprogress@0.2.0)(postcss@8.5.6)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1)
@@ -1175,7 +1181,7 @@ importers:
devDependencies:
unplugin-yaml:
specifier: ^3.0.7
version: 3.0.7(@nuxt/kit@4.0.3(magicast@0.3.5))(@nuxt/schema@4.0.3)(astro@5.10.1(@netlify/blobs@9.1.2)(@types/node@24.9.2)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(ioredis@5.7.0)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(esbuild@0.25.9)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(vite@7.1.12(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))
version: 3.0.7(@nuxt/kit@4.0.3(magicast@0.3.5))(@nuxt/schema@4.0.3)(astro@5.10.1(@netlify/blobs@9.1.2)(@types/node@24.9.2)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(encoding@0.1.13)(ioredis@5.7.0)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(esbuild@0.25.9)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(vite@7.1.12(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))
packages/injecta:
devDependencies:
@@ -1526,6 +1532,9 @@ importers:
nanoid:
specifier: ^5.1.6
version: 5.1.6
ofetch:
specifier: ^1.5.0
version: 1.5.0
pinia:
specifier: ^3.0.3
version: 3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))
@@ -1571,6 +1580,9 @@ importers:
unspeech:
specifier: ^0.1.7
version: 0.1.7
uuid:
specifier: ^13.0.0
version: 13.0.0
vaul-vue:
specifier: ^0.4.1
version: 0.4.1(reka-ui@2.6.0(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))
@@ -1582,7 +1594,7 @@ importers:
version: 4.6.3(vue@3.5.22(typescript@5.9.3))
vue-sonner:
specifier: ^2.0.9
version: 2.0.9(@nuxt/kit@4.0.3(magicast@0.3.5))(@nuxt/schema@4.0.3)(nuxt@4.0.3(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.0)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(bufferutil@4.0.9)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7))(eslint@9.39.0(jiti@2.5.1))(ioredis@5.7.0)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@6.4.1(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))(vue-tsc@3.1.2(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.1))
version: 2.0.9(@nuxt/kit@4.0.3(magicast@0.3.5))(@nuxt/schema@4.0.3)(nuxt@4.0.3(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.0)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(bufferutil@4.0.9)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7))(encoding@0.1.13)(eslint@9.39.0(jiti@2.5.1))(ioredis@5.7.0)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@6.4.1(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))(vue-tsc@3.1.2(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.1))
vue-tsc:
specifier: ^3.1.2
version: 3.1.2(typescript@5.9.3)
@@ -1691,7 +1703,7 @@ importers:
version: 1.2.4(@nuxt/kit@4.0.3(magicast@0.3.5))(@nuxt/schema@4.0.3)(esbuild@0.25.9)(rollup@4.52.5)(vite@6.4.1(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))
unplugin-yaml:
specifier: ^3.0.7
version: 3.0.7(@nuxt/kit@4.0.3(magicast@0.3.5))(@nuxt/schema@4.0.3)(astro@5.10.1(@netlify/blobs@9.1.2)(@types/node@24.9.2)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(ioredis@5.7.0)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(esbuild@0.25.9)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(vite@6.4.1(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))
version: 3.0.7(@nuxt/kit@4.0.3(magicast@0.3.5))(@nuxt/schema@4.0.3)(astro@5.10.1(@netlify/blobs@9.1.2)(@types/node@24.9.2)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(encoding@0.1.13)(ioredis@5.7.0)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(esbuild@0.25.9)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(vite@6.4.1(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))
vite:
specifier: ^6.4.1
version: 6.4.1(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1)
@@ -29213,7 +29225,7 @@ snapshots:
- yaml
optional: true
nuxt@4.0.3(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.0)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(bufferutil@4.0.9)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7))(eslint@9.39.0(jiti@2.5.1))(ioredis@5.7.0)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@6.4.1(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))(vue-tsc@3.1.2(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.1):
nuxt@4.0.3(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.0)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(bufferutil@4.0.9)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7))(encoding@0.1.13)(eslint@9.39.0(jiti@2.5.1))(ioredis@5.7.0)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@6.4.1(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))(vue-tsc@3.1.2(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.1):
dependencies:
'@nuxt/cli': 3.28.0(magicast@0.3.5)
'@nuxt/devalue': 2.0.2
@@ -29338,7 +29350,7 @@ snapshots:
- yaml
optional: true
nuxt@4.0.3(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.0)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(bufferutil@4.0.9)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7))(eslint@9.39.0(jiti@2.5.1))(ioredis@5.7.0)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@7.1.12(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))(vue-tsc@3.1.2(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.1):
nuxt@4.0.3(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.0)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(bufferutil@4.0.9)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7))(encoding@0.1.13)(eslint@9.39.0(jiti@2.5.1))(ioredis@5.7.0)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@7.1.12(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))(vue-tsc@3.1.2(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.1):
dependencies:
'@nuxt/cli': 3.28.0(magicast@0.3.5)
'@nuxt/devalue': 2.0.2
@@ -33032,7 +33044,7 @@ snapshots:
rollup: 4.52.5
vite: rolldown-vite@7.1.20(@types/node@24.9.2)(esbuild@0.25.9)(jiti@2.5.1)(less@4.4.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1)
unplugin-yaml@3.0.7(@nuxt/kit@4.0.3(magicast@0.3.5))(@nuxt/schema@4.0.3)(astro@5.10.1(@netlify/blobs@9.1.2)(@types/node@24.9.2)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(ioredis@5.7.0)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(esbuild@0.25.9)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(vite@6.4.1(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1)):
unplugin-yaml@3.0.7(@nuxt/kit@4.0.3(magicast@0.3.5))(@nuxt/schema@4.0.3)(astro@5.10.1(@netlify/blobs@9.1.2)(@types/node@24.9.2)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(encoding@0.1.13)(ioredis@5.7.0)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(esbuild@0.25.9)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(vite@6.4.1(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1)):
dependencies:
'@rollup/pluginutils': 5.3.0(rollup@4.52.5)
unplugin: 2.3.10
@@ -33046,7 +33058,7 @@ snapshots:
rollup: 4.52.5
vite: 6.4.1(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1)
unplugin-yaml@3.0.7(@nuxt/kit@4.0.3(magicast@0.3.5))(@nuxt/schema@4.0.3)(astro@5.10.1(@netlify/blobs@9.1.2)(@types/node@24.9.2)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(ioredis@5.7.0)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(esbuild@0.25.9)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(vite@7.1.12(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1)):
unplugin-yaml@3.0.7(@nuxt/kit@4.0.3(magicast@0.3.5))(@nuxt/schema@4.0.3)(astro@5.10.1(@netlify/blobs@9.1.2)(@types/node@24.9.2)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(encoding@0.1.13)(ioredis@5.7.0)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(esbuild@0.25.9)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(vite@7.1.12(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1)):
dependencies:
'@rollup/pluginutils': 5.3.0(rollup@4.52.5)
unplugin: 2.3.10
@@ -33698,17 +33710,17 @@ snapshots:
'@nuxt/schema': 4.0.3
nuxt: 4.0.3(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.0)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(bufferutil@4.0.9)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7))(encoding@0.1.13)(eslint@9.39.0(jiti@2.5.1))(ioredis@5.7.0)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown-vite@7.1.20(@types/node@24.9.2)(esbuild@0.25.9)(jiti@2.5.1)(less@4.4.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))(rolldown@1.0.0-beta.45)(rollup@2.79.2)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(utf-8-validate@5.0.10)(vue-tsc@3.1.2(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.1)
vue-sonner@2.0.9(@nuxt/kit@4.0.3(magicast@0.3.5))(@nuxt/schema@4.0.3)(nuxt@4.0.3(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.0)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(bufferutil@4.0.9)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7))(eslint@9.39.0(jiti@2.5.1))(ioredis@5.7.0)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@6.4.1(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))(vue-tsc@3.1.2(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.1)):
vue-sonner@2.0.9(@nuxt/kit@4.0.3(magicast@0.3.5))(@nuxt/schema@4.0.3)(nuxt@4.0.3(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.0)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(bufferutil@4.0.9)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7))(encoding@0.1.13)(eslint@9.39.0(jiti@2.5.1))(ioredis@5.7.0)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@6.4.1(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))(vue-tsc@3.1.2(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.1)):
optionalDependencies:
'@nuxt/kit': 4.0.3(magicast@0.3.5)
'@nuxt/schema': 4.0.3
nuxt: 4.0.3(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.0)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(bufferutil@4.0.9)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7))(eslint@9.39.0(jiti@2.5.1))(ioredis@5.7.0)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@6.4.1(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))(vue-tsc@3.1.2(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.1)
nuxt: 4.0.3(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.0)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(bufferutil@4.0.9)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7))(encoding@0.1.13)(eslint@9.39.0(jiti@2.5.1))(ioredis@5.7.0)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@6.4.1(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))(vue-tsc@3.1.2(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.1)
vue-sonner@2.0.9(@nuxt/kit@4.0.3(magicast@0.3.5))(@nuxt/schema@4.0.3)(nuxt@4.0.3(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.0)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(bufferutil@4.0.9)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7))(eslint@9.39.0(jiti@2.5.1))(ioredis@5.7.0)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@7.1.12(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))(vue-tsc@3.1.2(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.1)):
vue-sonner@2.0.9(@nuxt/kit@4.0.3(magicast@0.3.5))(@nuxt/schema@4.0.3)(nuxt@4.0.3(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.0)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(bufferutil@4.0.9)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7))(encoding@0.1.13)(eslint@9.39.0(jiti@2.5.1))(ioredis@5.7.0)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@7.1.12(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))(vue-tsc@3.1.2(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.1)):
optionalDependencies:
'@nuxt/kit': 4.0.3(magicast@0.3.5)
'@nuxt/schema': 4.0.3
nuxt: 4.0.3(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.0)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(bufferutil@4.0.9)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7))(eslint@9.39.0(jiti@2.5.1))(ioredis@5.7.0)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@7.1.12(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))(vue-tsc@3.1.2(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.1)
nuxt: 4.0.3(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.0)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(bufferutil@4.0.9)(db0@0.3.2(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7))(encoding@0.1.13)(eslint@9.39.0(jiti@2.5.1))(ioredis@5.7.0)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.45)(rollup@4.52.5)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(utf-8-validate@5.0.10)(vite@7.1.12(@types/node@24.9.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))(vue-tsc@3.1.2(typescript@5.9.3))(xml2js@0.6.2)(yaml@2.8.1)
vue-tsc@3.1.2(typescript@5.9.3):
dependencies:
+1
View File
@@ -4,6 +4,7 @@ export default defineConfig({
test: {
projects: [
'packages/injecta',
'packages/stage-ui',
],
},
})