From 7d1e3ed8841787200c083ed02b292480b9e04068 Mon Sep 17 00:00:00 2001 From: Wellshh Date: Thu, 13 Nov 2025 19:06:55 +0800 Subject: [PATCH 1/3] feat: use pandoc for typst convertion instead of tex2typst --- .dockerignore | 8 ++++++++ Dockerfile | 24 ++++++++++++++++++++++++ package.json | 1 + pnpm-lock.yaml | 16 ++++++++++++---- 4 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5f45d26 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +node_modules +*.md +dist +.git* +.vscode +.env +.DS_Store +test diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..82c12e4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM pandoc/core:latest AS base + +RUN apk add --no-cache curl nodejs npm \ + && npm install -g pnpm + +WORKDIR /app + +FROM base AS deps +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile + +FROM deps AS build +COPY . . +RUN pnpm build + +FROM base AS runtime +COPY --from=deps /app/node_modules /app/node_modules +COPY --from=build /app/.output /app/.output + +EXPOSE 300 +ENV NODE_ENV=production + +ENTRYPOINT [] +CMD ["node", ".output/server/index.mjs"] \ No newline at end of file diff --git a/package.json b/package.json index d4abeec..67b0d3e 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "image-js": "^1.0.0", "katex": "^0.16.23", "nuxt": "^4.1.2", + "tailwindcss": "^4.1.17", "tex2typst": "^0.4.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c51e1ac..8c16237 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: nuxt: specifier: ^4.1.2 version: 4.1.3(@parcel/watcher@2.5.1)(@types/node@24.7.1)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(eslint@9.37.0(jiti@2.6.1))(ioredis@5.8.1)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.52.4)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.9(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.1.1(typescript@5.9.3))(yaml@2.8.1) + tailwindcss: + specifier: ^4.1.17 + version: 4.1.17 tex2typst: specifier: ^0.4.0 version: 0.4.0 @@ -4796,6 +4799,9 @@ packages: tailwindcss@4.1.14: resolution: {integrity: sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==} + tailwindcss@4.1.17: + resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==} + tapable@2.3.0: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} @@ -6498,8 +6504,8 @@ snapshots: reka-ui: 2.5.1(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)) scule: 1.3.0 tailwind-merge: 3.3.1 - tailwind-variants: 3.1.1(tailwind-merge@3.3.1)(tailwindcss@4.1.14) - tailwindcss: 4.1.14 + tailwind-variants: 3.1.1(tailwind-merge@3.3.1)(tailwindcss@4.1.17) + tailwindcss: 4.1.17 tinyglobby: 0.2.15 typescript: 5.9.3 unplugin: 2.3.10 @@ -10582,14 +10588,16 @@ snapshots: tailwind-merge@3.3.1: {} - tailwind-variants@3.1.1(tailwind-merge@3.3.1)(tailwindcss@4.1.14): + tailwind-variants@3.1.1(tailwind-merge@3.3.1)(tailwindcss@4.1.17): dependencies: - tailwindcss: 4.1.14 + tailwindcss: 4.1.17 optionalDependencies: tailwind-merge: 3.3.1 tailwindcss@4.1.14: {} + tailwindcss@4.1.17: {} + tapable@2.3.0: {} tar-stream@3.1.7: From 6ae8d3124fce4ff65f45d1ca58dcd783c637c930 Mon Sep 17 00:00:00 2001 From: Wellshh Date: Sun, 16 Nov 2025 23:53:15 +0800 Subject: [PATCH 2/3] add pandoc manager --- app/composables/textProcessor.ts | 41 ++++++++++++++-- app/composables/types/pandoc.ts | 20 ++++++++ app/pages/ocr.vue | 2 +- package.json | 1 + pnpm-lock.yaml | 29 +++++++---- server/api/convert/typst.post.ts | 31 ++++++++++++ server/utils/pandocManager.ts | 83 ++++++++++++++++++++++++++++++++ 7 files changed, 194 insertions(+), 13 deletions(-) create mode 100644 app/composables/types/pandoc.ts create mode 100644 server/api/convert/typst.post.ts create mode 100644 server/utils/pandocManager.ts diff --git a/app/composables/textProcessor.ts b/app/composables/textProcessor.ts index 8c9aad7..f850edf 100644 --- a/app/composables/textProcessor.ts +++ b/app/composables/textProcessor.ts @@ -1,4 +1,5 @@ import { tex2typst } from 'tex2typst' +import { Latex2TypstTool } from './types/pandoc' /** * 包裹 LaTeX 代码 @@ -118,7 +119,41 @@ export function formatLatex(code: string): string { return result.filter(line => line.trim()).join('\n') } -export function convertToTypst(code: string) { +let lastInput: string | null = null +let lastOutput: string | null = null + +export async function convertToTypst(code: string, tool: Latex2TypstTool = Latex2TypstTool.Pandoc): Promise { const cleanedCode = code.replace(/~/g, '\\ ') - return tex2typst(cleanedCode) -} + const cacheKey = `${tool}:${cleanedCode.trim()}` + + if (lastInput === cacheKey && lastOutput !== null) { + if (import.meta.client) { + console.log(`[convertToTypst] Cache hit: same input as last conversion`) + } + return lastOutput + } + let result: string + switch (tool) { + case Latex2TypstTool.Pandoc: + const response = await $fetch<{ success: boolean; output: string }>('/api/convert/typst', { + method: 'POST', + body: { latex: cleanedCode } + }) + if (response.success) { + result = response.output + } else { + throw new Error('Pandoc conversion failed') + } + break + case Latex2TypstTool.Tex2Typst: + result = tex2typst(cleanedCode) + break + default: + throw new Error(`Unknown conversion tool: ${tool}`) + } + + lastInput = cacheKey + lastOutput = result + + return result +} \ No newline at end of file diff --git a/app/composables/types/pandoc.ts b/app/composables/types/pandoc.ts new file mode 100644 index 0000000..3e3be78 --- /dev/null +++ b/app/composables/types/pandoc.ts @@ -0,0 +1,20 @@ +export enum PandocStatus { + Ready = 'ready', + Processing = 'processing', + Result = 'result', + Error = 'error', + Timeout = 'timeout' +} + +export type PandocConvertOption = { + from?: string + to?: string + extraArgs?: string[] + timeout?: number + key?: string +} + +export enum Latex2TypstTool { + Tex2Typst = 'tex2typst', + Pandoc = 'pandoc', +} diff --git a/app/pages/ocr.vue b/app/pages/ocr.vue index 773a64d..013b4f4 100644 --- a/app/pages/ocr.vue +++ b/app/pages/ocr.vue @@ -78,7 +78,7 @@ function clear() { async function copyAsTypst() { try { - const typstCode = convertToTypst(latexCode.value) + const typstCode = await convertToTypst(latexCode.value) await navigator.clipboard.writeText(typstCode) toast?.add({ title: t('typstCode') + ' ' + t('copied'), diff --git a/package.json b/package.json index 2b5c6c6..fb6a7ad 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "image-js": "^1.0.0", "katex": "^0.16.23", "nuxt": "^4.1.2", + "p-limit": "^7.2.0", "tailwindcss": "^4.1.17", "tex2typst": "^0.4.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 009bff8..e7ab1f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: nuxt: specifier: ^4.1.2 version: 4.1.3(@parcel/watcher@2.5.1)(@types/node@24.7.1)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(eslint@9.37.0(jiti@2.6.1))(ioredis@5.8.1)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.52.4)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.9(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.1.1(typescript@5.9.3))(yaml@2.8.1) + p-limit: + specifier: ^7.2.0 + version: 7.2.0 tailwindcss: specifier: ^4.1.17 version: 4.1.17 @@ -4880,6 +4883,10 @@ packages: resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-limit@7.2.0: + resolution: {integrity: sha512-ATHLtwoTNDloHRFFxFJdHnG6n2WUeFjaR8XQMFdKIv0xkXjrER8/iG9iu265jOM95zXHAfv9oTkqhrfbIzosrQ==} + engines: {node: '>=20'} + p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} @@ -6609,7 +6616,7 @@ snapshots: '@babel/core': 7.28.4 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-wrap-function': 7.28.3 - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -6640,8 +6647,8 @@ snapshots: '@babel/helper-wrap-function@7.28.3': dependencies: '@babel/template': 7.27.2 - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color @@ -6689,7 +6696,7 @@ snapshots: dependencies: '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -6733,7 +6740,7 @@ snapshots: '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.4) - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -6780,7 +6787,7 @@ snapshots: '@babel/helper-globals': 7.28.0 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4) - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -6851,7 +6858,7 @@ snapshots: '@babel/core': 7.28.4 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -6937,7 +6944,7 @@ snapshots: '@babel/helper-plugin-utils': 7.27.1 '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.28.4) '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.4) - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -7147,7 +7154,7 @@ snapshots: dependencies: '@babel/core': 7.28.4 '@babel/helper-plugin-utils': 7.27.1 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 esutils: 2.0.3 '@babel/runtime@7.28.4': {} @@ -11981,6 +11988,10 @@ snapshots: dependencies: yocto-queue: 1.2.1 + p-limit@7.2.0: + dependencies: + yocto-queue: 1.2.1 + p-locate@5.0.0: dependencies: p-limit: 3.1.0 diff --git a/server/api/convert/typst.post.ts b/server/api/convert/typst.post.ts new file mode 100644 index 0000000..0db9c07 --- /dev/null +++ b/server/api/convert/typst.post.ts @@ -0,0 +1,31 @@ +import { getPandocManager } from '../../utils/pandocManager' + +export default defineEventHandler(async (event) => { + const body = await readBody(event) + const { latex } = body + + if (!latex || typeof latex !== 'string') { + throw createError({ + statusCode: 400, + message: 'Missing or invalid latex parameter' + }) + } + + try { + const manager = getPandocManager() + const typstCode = await manager.convert(latex, { + timeout: 20000 + }) + + return { + success: true, + output: typstCode + } + } catch (error) { + throw createError({ + statusCode: 500, + message: error instanceof Error ? error.message : 'Pandoc conversion failed' + }) + } +}) + diff --git a/server/utils/pandocManager.ts b/server/utils/pandocManager.ts new file mode 100644 index 0000000..be7c973 --- /dev/null +++ b/server/utils/pandocManager.ts @@ -0,0 +1,83 @@ +/** + * External pandoc process manager module + */ +import type { PandocConvertOption} from "../../app/composables/types/pandoc"; +import {spawn, type ChildProcess} from 'child_process'; +import pLimit from 'p-limit' + +export class PandocManager { + private _concurrencyLimit: ReturnType + + constructor(concurrency: number = 3){ + this._concurrencyLimit = pLimit(concurrency) + } + + /** + * Convert Latex to Typst + */ + public async convert(latex: string, options: PandocConvertOption = {}): Promise{ + return this._concurrencyLimit(async () => { + return new Promise((resolve, reject) => { + const timeout = options.timeout || 10000 + const from = options.from || 'latex' + const to = options.to || 'typst' + const extraArgs = options.extraArgs || [] + + const process = spawn('pandoc',[`--from=${from}`, `--to=${to}`, '--wrap=none',...extraArgs],{stdio: ['pipe','pipe','pipe']}) + let stdoutBuffer = '' + let stderrBuffer = '' + + const timeoutId = setTimeout(() => { + process.kill('SIGTERM') + reject(new Error(`Pandoc convertion timeout after ${timeout}ms`)) + }, timeout) + process.stdout?.on('data', (data: Buffer) => {stdoutBuffer += data.toString()}) + process.stderr?.on('data', (data: Buffer) => {stderrBuffer += data.toString()}) + process.on('close',(code) => { + clearTimeout(timeoutId) + if (code === 0) { + resolve(stdoutBuffer.trim()) + } else { + reject(new Error( + `Pandoc conversion failed (code ${code}): ${stderrBuffer || 'Unknown error'}` + )) + } + }) + process.on('error', (error) => { + clearTimeout(timeoutId) + reject(new Error(`Failed to start Pandoc: ${error.message}`)) + }) + if (process.stdin){ + process.stdin.write(latex) + process.stdin.end() + } + }) + }) + } + + public getQueueStatus(): { + pending: number + active: number + total: number + isIdle: boolean + } { + const pending = this._concurrencyLimit.pendingCount ?? 0 + const active = this._concurrencyLimit.activeCount ?? 0 + + return { + pending, + active, + total: pending + active, + isIdle: pending === 0 && active === 0 + } + } + +} + +let pandocManager: PandocManager | null = null +export function getPandocManager(): PandocManager{ + if (!pandocManager){ + pandocManager = new PandocManager(3) + } + return pandocManager +} \ No newline at end of file From 907ebd0f0f548e91b36478a13b2f4042e9942b8a Mon Sep 17 00:00:00 2001 From: Wellshh Date: Wed, 19 Nov 2025 23:15:48 +0800 Subject: [PATCH 3/3] finish testing & dockerlize testing --- .github/workflows/ci.yml | 22 +- Dockerfile | 35 +- app/composables/textProcessor.ts | 36 +- docker-compose.yaml | 15 + nuxt.config.ts | 3 +- package.json | 8 +- pnpm-lock.yaml | 487 ++++++++++++++++++++++++++ test/nuxt/textProcessor.cache.spec.ts | 71 ++++ test/unit/pandocManager.spec.ts | 153 ++++++++ vitest.config.ts | 24 ++ 10 files changed, 834 insertions(+), 20 deletions(-) create mode 100644 docker-compose.yaml create mode 100644 test/nuxt/textProcessor.cache.spec.ts create mode 100644 test/unit/pandocManager.spec.ts create mode 100644 vitest.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c48fec..679c96e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,12 @@ jobs: - name: Checkout uses: actions/checkout@v5 + - name: Setup QEMU + uses: docker/setup-qemu-action@v3 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Install pnpm uses: pnpm/action-setup@v4 @@ -27,8 +33,16 @@ jobs: - name: Install dependencies run: pnpm install - - name: Lint - run: pnpm run lint + - name: Build docker image for tests + run: docker build -t texo-test --target development . + + - name: Lint in docker + run: docker run --rm texo-test pnpm run lint + + - name: Typecheck in docker + run: docker run --rm texo-test pnpm run typecheck - - name: Typecheck - run: pnpm run typecheck + - name: Test in docker + env: + FORCE_REAL_PANDOC: 1 + run: docker run --rm -e FORCE_REAL_PANDOC texo-test pnpm vitest run diff --git a/Dockerfile b/Dockerfile index 82c12e4..31a0a18 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,43 @@ FROM pandoc/core:latest AS base - -RUN apk add --no-cache curl nodejs npm \ +RUN apk add --no-cache curl nodejs npm git \ && npm install -g pnpm +ENTRYPOINT [] + WORKDIR /app FROM base AS deps COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile -FROM deps AS build +# Usage: docker build --target development -t my-app-dev . +FROM base AS development +ENV NODE_ENV=development +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +EXPOSE 3000 +CMD ["pnpm", "dev"] + +# Usage: docker build --target test . +FROM development AS test +ENV NODE_ENV=test +RUN pnpm test + +FROM base AS build +COPY --from=deps /app/node_modules ./node_modules COPY . . RUN pnpm build -FROM base AS runtime -COPY --from=deps /app/node_modules /app/node_modules -COPY --from=build /app/.output /app/.output +FROM base AS prod-deps +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --prod --frozen-lockfile -EXPOSE 300 +FROM base AS runtime ENV NODE_ENV=production -ENTRYPOINT [] +COPY --from=prod-deps /app/node_modules /app/node_modules +COPY --from=build /app/.output /app/.output + +EXPOSE 3000 CMD ["node", ".output/server/index.mjs"] \ No newline at end of file diff --git a/app/composables/textProcessor.ts b/app/composables/textProcessor.ts index 9a519d3..90b9e00 100644 --- a/app/composables/textProcessor.ts +++ b/app/composables/textProcessor.ts @@ -43,16 +43,24 @@ export function formatLatex(code: string): string { let lastInput: string | null = null let lastOutput: string | null = null +let clientModeOverride: boolean | null = null + +function isClientRuntime(): boolean { + if (clientModeOverride !== null) { + return clientModeOverride + } + return import.meta.client +} export async function convertToTypst(code: string, tool: Latex2TypstTool = Latex2TypstTool.Pandoc): Promise { const cleanedCode = code.replace(/~/g, '\\ ') const cacheKey = `${tool}:${cleanedCode.trim()}` - if (lastInput === cacheKey && lastOutput !== null) { - if (import.meta.client) { + if (isClientRuntime()){ + if (lastInput === cacheKey && lastOutput !== null) { console.log(`[convertToTypst] Cache hit: same input as last conversion`) + return lastOutput } - return lastOutput } let result: string switch (tool) { @@ -74,8 +82,24 @@ export async function convertToTypst(code: string, tool: Latex2TypstTool = Latex throw new Error(`Unknown conversion tool: ${tool}`) } - lastInput = cacheKey - lastOutput = result - + if (isClientRuntime()){ + lastInput = cacheKey + lastOutput = result + } return result +} + +/** + * Test helper: reset cached conversion state. + */ +export function __resetConvertCache(): void { + lastInput = null + lastOutput = null +} + +/** + * Test helper: override client runtime detection. + */ +export function __setConvertClientMode(value: boolean | null): void { + clientModeOverride = value } \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..f6e0709 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,15 @@ +services: + app: + build: + context: . + target: development + volumes: + - .:/app + - /app/node_modules + environment: + - HOST=0.0.0.0 + - NUXT_HOST=0.0.0.0 + - PORT=3000 + - NODE_ENV=development + ports: + - "3000:3000" \ No newline at end of file diff --git a/nuxt.config.ts b/nuxt.config.ts index c46aab5..398d75d 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -4,7 +4,8 @@ export default defineNuxtConfig({ '@nuxt/eslint', '@nuxt/ui', '@nuxtjs/i18n', - '@vite-pwa/nuxt' + '@vite-pwa/nuxt', + '@nuxt/test-utils/module', ], ssr: true, components: true, diff --git a/package.json b/package.json index fb6a7ad..8f4643e 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "preview": "nuxt preview", "postinstall": "nuxt prepare", "lint": "eslint .", - "typecheck": "nuxt typecheck" + "typecheck": "nuxt typecheck", + "test:nuxt": "npx vitest run --project nuxt", + "test": "npx vitest run" }, "dependencies": { "@giscus/vue": "^3.1.1", @@ -28,8 +30,12 @@ }, "devDependencies": { "@nuxt/eslint": "^1.9.0", + "@nuxt/test-utils": "^3.20.1", + "@vue/test-utils": "^2.4.6", "eslint": "^9.37.0", + "happy-dom": "^20.0.10", "typescript": "^5.9.3", + "vitest": "^4.0.10", "vue-tsc": "^3.1.0" }, "resolutions": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e7ab1f6..9df8216 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,12 +57,24 @@ importers: '@nuxt/eslint': specifier: ^1.9.0 version: 1.9.0(@typescript-eslint/utils@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.22)(eslint@9.37.0(jiti@2.6.1))(magicast@0.3.5)(typescript@5.9.3)(vite@7.1.9(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1)) + '@nuxt/test-utils': + specifier: ^3.20.1 + version: 3.20.1(@vue/test-utils@2.4.6)(happy-dom@20.0.10)(magicast@0.3.5)(typescript@5.9.3)(vitest@4.0.10(@types/node@24.7.1)(happy-dom@20.0.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1)) + '@vue/test-utils': + specifier: ^2.4.6 + version: 2.4.6 eslint: specifier: ^9.37.0 version: 9.37.0(jiti@2.6.1) + happy-dom: + specifier: ^20.0.10 + version: 20.0.10 typescript: specifier: ^5.9.3 version: 5.9.3 + vitest: + specifier: ^4.0.10 + version: 4.0.10(@types/node@24.7.1)(happy-dom@20.0.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1) vue-tsc: specifier: ^3.1.0 version: 3.1.1(typescript@5.9.3) @@ -1327,6 +1339,42 @@ packages: engines: {node: '>=18.12.0'} hasBin: true + '@nuxt/test-utils@3.20.1': + resolution: {integrity: sha512-SNS8rCoO5vOHkWbAyGU/LgX3p41VHUq6u+7JEc3jNq9YAu/pA9V31AWJcPCfiZtw1PTJzk0TT+H8dhIHPFY2IQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@cucumber/cucumber': ^10.3.1 || >=11.0.0 + '@jest/globals': ^29.5.0 || >=30.0.0 + '@playwright/test': ^1.43.1 + '@testing-library/vue': ^7.0.0 || ^8.0.1 + '@vitest/ui': '*' + '@vue/test-utils': ^2.4.2 + happy-dom: '*' + jsdom: '*' + playwright-core: ^1.43.1 + vitest: ^3.2.0 + peerDependenciesMeta: + '@cucumber/cucumber': + optional: true + '@jest/globals': + optional: true + '@playwright/test': + optional: true + '@testing-library/vue': + optional: true + '@vitest/ui': + optional: true + '@vue/test-utils': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + playwright-core: + optional: true + vitest: + optional: true + '@nuxt/ui@4.0.1': resolution: {integrity: sha512-mtY8wairYw2WXotCYxXG0CmxbqyJWaMHYbes3p+vFaOJ2kdQHQh7QM/7ziQeZHxVNHciBcWayi6G+55ok/kHAQ==} hasBin: true @@ -1372,6 +1420,9 @@ packages: resolution: {integrity: sha512-2h/6Y4ke+mYq3RrV71erTBn1HzKKKPGEJrzYW6GA8SAc91zb7jqyfRkElG95Cei+2+6XJrt73Djys5qTc0tCUw==} engines: {node: '>=20.11.1'} + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} @@ -2332,6 +2383,12 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@0.0.39': resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} @@ -2341,6 +2398,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node@20.19.25': + resolution: {integrity: sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==} + '@types/node@24.7.1': resolution: {integrity: sha512-CmyhGZanP88uuC5GpWU9q+fI61j2SkhO3UGMUdfYRE6Bcy0ccyzn1Rqj9YAB/ZY4kOXmNf0ocah5GtphmLMP6Q==} @@ -2363,6 +2423,9 @@ packages: '@types/web-bluetooth@0.0.21': resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + '@types/whatwg-mimetype@3.0.2': + resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} + '@typescript-eslint/eslint-plugin@8.46.0': resolution: {integrity: sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2553,6 +2616,35 @@ packages: vite: ^5.0.0 || ^6.0.0 || ^7.0.0 vue: ^3.2.25 + '@vitest/expect@4.0.10': + resolution: {integrity: sha512-3QkTX/lK39FBNwARCQRSQr0TP9+ywSdxSX+LgbJ2M1WmveXP72anTbnp2yl5fH+dU6SUmBzNMrDHs80G8G2DZg==} + + '@vitest/mocker@4.0.10': + resolution: {integrity: sha512-e2OfdexYkjkg8Hh3L9NVEfbwGXq5IZbDovkf30qW2tOh7Rh9sVtmSr2ztEXOFbymNxS4qjzLXUQIvATvN4B+lg==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.0.10': + resolution: {integrity: sha512-99EQbpa/zuDnvVjthwz5bH9o8iPefoQZ63WV8+bsRJZNw3qQSvSltfut8yu1Jc9mqOYi7pEbsKxYTi/rjaq6PA==} + + '@vitest/runner@4.0.10': + resolution: {integrity: sha512-EXU2iSkKvNwtlL8L8doCpkyclw0mc/t4t9SeOnfOFPyqLmQwuceMPA4zJBa6jw0MKsZYbw7kAn+gl7HxrlB8UQ==} + + '@vitest/snapshot@4.0.10': + resolution: {integrity: sha512-2N4X2ZZl7kZw0qeGdQ41H0KND96L3qX1RgwuCfy6oUsF2ISGD/HpSbmms+CkIOsQmg2kulwfhJ4CI0asnZlvkg==} + + '@vitest/spy@4.0.10': + resolution: {integrity: sha512-AsY6sVS8OLb96GV5RoG8B6I35GAbNrC49AO+jNRF9YVGb/g9t+hzNm1H6kD0NDp8tt7VJLs6hb7YMkDXqu03iw==} + + '@vitest/utils@4.0.10': + resolution: {integrity: sha512-kOuqWnEwZNtQxMKg3WmPK1vmhZu9WcoX69iwWjVz+jvKTsF1emzsv3eoPcDr6ykA3qP2bsCQE7CwqfNtAVzsmg==} + '@volar/language-core@2.4.23': resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==} @@ -2647,6 +2739,9 @@ packages: '@vue/shared@3.5.22': resolution: {integrity: sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==} + '@vue/test-utils@2.4.6': + resolution: {integrity: sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==} + '@vueuse/core@10.11.1': resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==} @@ -2720,6 +2815,10 @@ packages: peerDependencies: vue: ^3.5.0 + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + abbrev@3.0.1: resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} engines: {node: ^18.17.0 || >=20.5.0} @@ -2813,6 +2912,10 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-kit@2.1.3: resolution: {integrity: sha512-TH+b3Lv6pUjy/Nu0m6A2JULtdzLpmqF9x1Dhj00ZoEiML8qvVA9j1flkzTKNYgdEhWrjDwtWNpyyCUbfQe514g==} engines: {node: '>=20.19.0'} @@ -2963,6 +3066,14 @@ packages: magicast: optional: true + c12@3.3.2: + resolution: {integrity: sha512-QkikB2X5voO1okL3QsES0N690Sn/K9WokXqUsDQsWy5SnYb+psYQFGA10iy1bZHj3fjISKsI67Q90gruvWWM3A==} + peerDependencies: + magicast: '*' + peerDependenciesMeta: + magicast: + optional: true + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -2989,6 +3100,10 @@ packages: caniuse-lite@1.0.30001749: resolution: {integrity: sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q==} + chai@6.2.1: + resolution: {integrity: sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -3055,6 +3170,10 @@ packages: colortranslator@5.0.0: resolution: {integrity: sha512-Z3UPUKasUVDFCDYAjP2fmlVRf1jFHJv1izAmPjiOa0OCIw1W7iC8PZ2GsoDa8uZv+mKyWopxxStT9q05+27h7w==} + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + commander@11.1.0: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} @@ -3093,6 +3212,9 @@ packages: confbox@0.2.2: resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + consola@3.4.2: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} @@ -3344,6 +3466,11 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + editorconfig@1.0.4: + resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==} + engines: {node: '>=14'} + hasBin: true + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -3657,9 +3784,20 @@ packages: resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==} engines: {node: ^18.19.0 || >=20.5.0} + expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + engines: {node: '>=12.0.0'} + exsolve@1.0.7: resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + + fake-indexeddb@6.2.5: + resolution: {integrity: sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==} + engines: {node: '>=18'} + fast-bmp@4.0.1: resolution: {integrity: sha512-+KtMijJj+uA8Sl6EXAnhza7US7EXSY5Z9NeiJwT1wopVUksyLMXL5iFmn9FjY8FdkstOkpJI9RuEVXkGpIPSwg==} @@ -3929,6 +4067,10 @@ packages: h3@1.15.4: resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==} + happy-dom@20.0.10: + resolution: {integrity: sha512-6umCCHcjQrhP5oXhrHQQvLB0bwb1UzHAHdsXy+FjtKoYjUhmNZsQL8NivwM1vDvNEChJabVrUYxUnp/ZdYmy2g==} + engines: {node: '>=20.0.0'} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -4031,6 +4173,9 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + ini@4.1.1: resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -4272,6 +4417,15 @@ packages: jpeg-js@0.4.4: resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} + js-beautify@1.15.4: + resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} + engines: {node: '>=14'} + hasBin: true + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + js-priority-queue@0.1.5: resolution: {integrity: sha512-2dPmJT4GbXUpob7AZDR1wFMKz3Biy6oW69mwt5PTtdeoOgDin1i0p5gUV9k0LFeUxDpwkfr+JGMZDpcprjiY5w==} @@ -4523,6 +4677,9 @@ packages: magic-string@0.30.19: resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.3.5: resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} @@ -4587,6 +4744,10 @@ packages: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} + minimatch@9.0.1: + resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} + engines: {node: '>=16 || 14 >=14.17'} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -4738,6 +4899,11 @@ packages: node-releases@2.0.23: resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==} + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + nopt@8.1.0: resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==} engines: {node: ^18.17.0 || >=20.5.0} @@ -5214,6 +5380,9 @@ packages: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + protobufjs@7.5.4: resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} engines: {node: '>=12.0.0'} @@ -5505,6 +5674,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -5578,6 +5750,9 @@ packages: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} @@ -5589,6 +5764,9 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} @@ -5765,6 +5943,12 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.0.1: resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} @@ -5772,6 +5956,10 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyrainbow@3.0.3: + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -5871,6 +6059,9 @@ packages: unctx@2.4.1: resolution: {integrity: sha512-AbaYw0Nm4mK4qjhns67C+kgxR2YWiwlDBPzxrN8h8C6VtAdCgditAY5Dezu3IJy4XVqAnbrXt9oQJvsn3fyozg==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.14.0: resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==} @@ -6205,12 +6396,52 @@ packages: yaml: optional: true + vitest-environment-nuxt@1.0.1: + resolution: {integrity: sha512-eBCwtIQriXW5/M49FjqNKfnlJYlG2LWMSNFsRVKomc8CaMqmhQPBS5LZ9DlgYL9T8xIVsiA6RZn2lk7vxov3Ow==} + + vitest@4.0.10: + resolution: {integrity: sha512-2Fqty3MM9CDwOVet/jaQalYlbcjATZwPYGcqpiYQqgQ/dLC7GuHdISKgTYIVF/kaishKxLzleKWWfbSDklyIKg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.10 + '@vitest/browser-preview': 4.0.10 + '@vitest/browser-webdriverio': 4.0.10 + '@vitest/ui': 4.0.10 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} vue-bundle-renderer@2.2.0: resolution: {integrity: sha512-sz/0WEdYH1KfaOm0XaBmRZOWgYTEvUDt6yPYaUzl4E52qzgWLlknaPPTTZmp6benaPTlQAI/hN1x3tAzZygycg==} + vue-component-type-helpers@2.2.12: + resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==} + vue-component-type-helpers@3.1.1: resolution: {integrity: sha512-B0kHv7qX6E7+kdc5nsaqjdGZ1KwNKSUQDWGy7XkTYT7wFsOpkEyaJ1Vq79TjwrrtuLRgizrTV7PPuC4rRQo+vw==} @@ -6268,6 +6499,10 @@ packages: webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -6304,6 +6539,11 @@ packages: engines: {node: ^18.17.0 || >=20.5.0} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -8086,6 +8326,40 @@ snapshots: transitivePeerDependencies: - magicast + '@nuxt/test-utils@3.20.1(@vue/test-utils@2.4.6)(happy-dom@20.0.10)(magicast@0.3.5)(typescript@5.9.3)(vitest@4.0.10(@types/node@24.7.1)(happy-dom@20.0.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))': + dependencies: + '@nuxt/kit': 4.1.3(magicast@0.3.5) + c12: 3.3.2(magicast@0.3.5) + consola: 3.4.2 + defu: 6.1.4 + destr: 2.0.5 + estree-walker: 3.0.3 + fake-indexeddb: 6.2.5 + get-port-please: 3.2.0 + h3: 1.15.4 + local-pkg: 1.1.2 + magic-string: 0.30.19 + node-fetch-native: 1.6.7 + node-mock-http: 1.0.3 + ofetch: 1.4.1 + pathe: 2.0.3 + perfect-debounce: 2.0.0 + radix3: 1.1.2 + scule: 1.3.0 + std-env: 3.10.0 + tinyexec: 1.0.1 + ufo: 1.6.1 + unplugin: 2.3.10 + vitest-environment-nuxt: 1.0.1(@vue/test-utils@2.4.6)(happy-dom@20.0.10)(magicast@0.3.5)(typescript@5.9.3)(vitest@4.0.10(@types/node@24.7.1)(happy-dom@20.0.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1)) + vue: 3.5.22(typescript@5.9.3) + optionalDependencies: + '@vue/test-utils': 2.4.6 + happy-dom: 20.0.10 + vitest: 4.0.10(@types/node@24.7.1)(happy-dom@20.0.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1) + transitivePeerDependencies: + - magicast + - typescript + '@nuxt/ui@4.0.1(@babel/parser@7.28.5)(change-case@5.4.4)(db0@0.3.4)(embla-carousel@8.6.0)(ioredis@5.8.1)(magicast@0.3.5)(typescript@5.9.3)(vite@7.1.9(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.6.3(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))(zod@4.1.12)': dependencies: '@ai-sdk/vue': 2.0.68(vue@3.5.22(typescript@5.9.3))(zod@4.1.12) @@ -8304,6 +8578,8 @@ snapshots: - uploadthing - vue + '@one-ini/wasm@0.1.1': {} + '@opentelemetry/api@1.9.0': {} '@oxc-minify/binding-android-arm64@0.94.0': @@ -8968,12 +9244,23 @@ snapshots: tslib: 2.8.1 optional: true + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + '@types/estree@0.0.39': {} '@types/estree@1.0.8': {} '@types/json-schema@7.0.15': {} + '@types/node@20.19.25': + dependencies: + undici-types: 6.21.0 + '@types/node@24.7.1': dependencies: undici-types: 7.14.0 @@ -8992,6 +9279,8 @@ snapshots: '@types/web-bluetooth@0.0.21': {} + '@types/whatwg-mimetype@3.0.2': {} + '@typescript-eslint/eslint-plugin@8.46.0(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -9202,6 +9491,45 @@ snapshots: vite: 7.1.9(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1) vue: 3.5.22(typescript@5.9.3) + '@vitest/expect@4.0.10': + dependencies: + '@standard-schema/spec': 1.0.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.10 + '@vitest/utils': 4.0.10 + chai: 6.2.1 + tinyrainbow: 3.0.3 + + '@vitest/mocker@4.0.10(vite@7.1.9(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 4.0.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.1.9(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1) + + '@vitest/pretty-format@4.0.10': + dependencies: + tinyrainbow: 3.0.3 + + '@vitest/runner@4.0.10': + dependencies: + '@vitest/utils': 4.0.10 + pathe: 2.0.3 + + '@vitest/snapshot@4.0.10': + dependencies: + '@vitest/pretty-format': 4.0.10 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.0.10': {} + + '@vitest/utils@4.0.10': + dependencies: + '@vitest/pretty-format': 4.0.10 + tinyrainbow: 3.0.3 + '@volar/language-core@2.4.23': dependencies: '@volar/source-map': 2.4.23 @@ -9357,6 +9685,11 @@ snapshots: '@vue/shared@3.5.22': {} + '@vue/test-utils@2.4.6': + dependencies: + js-beautify: 1.15.4 + vue-component-type-helpers: 2.2.12 + '@vueuse/core@10.11.1(vue@3.5.22(typescript@5.9.3))': dependencies: '@types/web-bluetooth': 0.0.20 @@ -9415,6 +9748,8 @@ snapshots: dependencies: vue: 3.5.22(typescript@5.9.3) + abbrev@2.0.0: {} + abbrev@3.0.1: {} abort-controller@3.0.0: @@ -9520,6 +9855,8 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + assertion-error@2.0.1: {} + ast-kit@2.1.3: dependencies: '@babel/parser': 7.28.4 @@ -9666,6 +10003,23 @@ snapshots: optionalDependencies: magicast: 0.3.5 + c12@3.3.2(magicast@0.3.5): + dependencies: + chokidar: 4.0.3 + confbox: 0.2.2 + defu: 6.1.4 + dotenv: 17.2.3 + exsolve: 1.0.8 + giget: 2.0.0 + jiti: 2.6.1 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 2.0.0 + pkg-types: 2.3.0 + rc9: 2.1.2 + optionalDependencies: + magicast: 0.3.5 + cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -9696,6 +10050,8 @@ snapshots: caniuse-lite@1.0.30001749: {} + chai@6.2.1: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -9766,6 +10122,8 @@ snapshots: colortranslator@5.0.0: {} + commander@10.0.1: {} + commander@11.1.0: {} commander@2.20.3: {} @@ -9794,6 +10152,11 @@ snapshots: confbox@0.2.2: {} + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + consola@3.4.2: {} convert-source-map@2.0.0: {} @@ -10024,6 +10387,13 @@ snapshots: eastasianwidth@0.2.0: {} + editorconfig@1.0.4: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.1 + semver: 7.7.3 + ee-first@1.1.1: {} ejs@3.1.10: @@ -10460,8 +10830,14 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.2 + expect-type@1.2.2: {} + exsolve@1.0.7: {} + exsolve@1.0.8: {} + + fake-indexeddb@6.2.5: {} + fast-bmp@4.0.1: dependencies: iobuffer: 6.0.1 @@ -10770,6 +11146,12 @@ snapshots: ufo: 1.6.1 uncrypto: 0.1.3 + happy-dom@20.0.10: + dependencies: + '@types/node': 20.19.25 + '@types/whatwg-mimetype': 3.0.2 + whatwg-mimetype: 3.0.0 + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -10881,6 +11263,8 @@ snapshots: inherits@2.0.4: {} + ini@1.3.8: {} + ini@4.1.1: {} internal-slot@1.1.0: @@ -11106,6 +11490,16 @@ snapshots: jpeg-js@0.4.4: {} + js-beautify@1.15.4: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.4 + glob: 10.4.5 + js-cookie: 3.0.5 + nopt: 7.2.1 + + js-cookie@3.0.5: {} + js-priority-queue@0.1.5: {} js-tokens@4.0.0: {} @@ -11338,6 +11732,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.3.5: dependencies: '@babel/parser': 7.28.4 @@ -11389,6 +11787,10 @@ snapshots: dependencies: brace-expansion: 2.0.2 + minimatch@9.0.1: + dependencies: + brace-expansion: 2.0.2 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.2 @@ -11636,6 +12038,10 @@ snapshots: node-releases@2.0.23: {} + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + nopt@8.1.0: dependencies: abbrev: 3.0.1 @@ -12272,6 +12678,8 @@ snapshots: kleur: 3.0.3 sisteransi: 1.0.5 + proto-list@1.2.4: {} + protobufjs@7.5.4: dependencies: '@protobufjs/aspromise': 1.1.2 @@ -12678,6 +13086,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@4.1.0: {} simple-git@3.28.0: @@ -12738,12 +13148,16 @@ snapshots: stable-hash-x@0.2.0: {} + stackback@0.0.2: {} + standard-as-callback@2.1.0: {} statuses@2.0.1: {} statuses@2.0.2: {} + std-env@3.10.0: {} + std-env@3.9.0: {} stop-iteration-iterator@1.1.0: @@ -12949,6 +13363,10 @@ snapshots: tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + tinyexec@1.0.1: {} tinyglobby@0.2.15: @@ -12956,6 +13374,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyrainbow@3.0.3: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -13055,6 +13475,8 @@ snapshots: magic-string: 0.30.19 unplugin: 2.3.10 + undici-types@6.21.0: {} + undici-types@7.14.0: {} undici@7.16.0: {} @@ -13404,12 +13826,70 @@ snapshots: terser: 5.44.0 yaml: 2.8.1 + vitest-environment-nuxt@1.0.1(@vue/test-utils@2.4.6)(happy-dom@20.0.10)(magicast@0.3.5)(typescript@5.9.3)(vitest@4.0.10(@types/node@24.7.1)(happy-dom@20.0.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1)): + dependencies: + '@nuxt/test-utils': 3.20.1(@vue/test-utils@2.4.6)(happy-dom@20.0.10)(magicast@0.3.5)(typescript@5.9.3)(vitest@4.0.10(@types/node@24.7.1)(happy-dom@20.0.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1)) + transitivePeerDependencies: + - '@cucumber/cucumber' + - '@jest/globals' + - '@playwright/test' + - '@testing-library/vue' + - '@vitest/ui' + - '@vue/test-utils' + - happy-dom + - jsdom + - magicast + - playwright-core + - typescript + - vitest + + vitest@4.0.10(@types/node@24.7.1)(happy-dom@20.0.10)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1): + dependencies: + '@vitest/expect': 4.0.10 + '@vitest/mocker': 4.0.10(vite@7.1.9(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1)) + '@vitest/pretty-format': 4.0.10 + '@vitest/runner': 4.0.10 + '@vitest/snapshot': 4.0.10 + '@vitest/spy': 4.0.10 + '@vitest/utils': 4.0.10 + debug: 4.4.3 + es-module-lexer: 1.7.0 + expect-type: 1.2.2 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.1.9(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.7.1 + happy-dom: 20.0.10 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vscode-uri@3.1.0: {} vue-bundle-renderer@2.2.0: dependencies: ufo: 1.6.1 + vue-component-type-helpers@2.2.12: {} + vue-component-type-helpers@3.1.1: {} vue-demi@0.14.10(vue@3.5.22(typescript@5.9.3)): @@ -13464,6 +13944,8 @@ snapshots: webpack-virtual-modules@0.6.2: {} + whatwg-mimetype@3.0.0: {} + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -13526,6 +14008,11 @@ snapshots: dependencies: isexe: 3.1.1 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} workbox-background-sync@7.3.0: diff --git a/test/nuxt/textProcessor.cache.spec.ts b/test/nuxt/textProcessor.cache.spec.ts new file mode 100644 index 0000000..0f8bf0e --- /dev/null +++ b/test/nuxt/textProcessor.cache.spec.ts @@ -0,0 +1,71 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' +import { convertToTypst, __resetConvertCache, __setConvertClientMode } from '../../app/composables/textProcessor' +import { Latex2TypstTool } from '../../app/composables/types/pandoc' + +const successResponse = { success: true, output: 'typst-output' } + +describe('convertToTypst cache behavior (nuxt env)', () => { + let fetchStub: ReturnType + + beforeEach(() => { + fetchStub = vi.fn().mockResolvedValue(successResponse) + vi.stubGlobal('$fetch', fetchStub) + __resetConvertCache() + __setConvertClientMode(true) + }) + + afterEach(() => { + vi.unstubAllGlobals() + vi.restoreAllMocks() + __resetConvertCache() + __setConvertClientMode(null) + }) + + it('reuses cache for the same client and same payload', async () => { + const first = await convertToTypst('\\alpha', Latex2TypstTool.Pandoc) + const second = await convertToTypst('\\alpha') + + expect(first).toBe('typst-output') + expect(second).toBe('typst-output') + expect(fetchStub).toHaveBeenCalledTimes(1) + }) + + it('does not share cache across simulated clients (cache reset)', async () => { + await convertToTypst('\\alpha') + __resetConvertCache() + const fresh = await convertToTypst('\\alpha') + + expect(fresh).toBe('typst-output') + expect(fetchStub).toHaveBeenCalledTimes(2) + }) + + it('does not cache when payload differs', async () => { + await convertToTypst('\\alpha') + await convertToTypst('\\beta + 1') + + expect(fetchStub).toHaveBeenCalledTimes(2) + }) + + it('bypasses cache when forced into non-client mode', async () => { + __setConvertClientMode(false) + fetchStub.mockResolvedValueOnce({ success: true, output: 'first' }) + fetchStub.mockResolvedValueOnce({ success: true, output: 'second' }) + + const first = await convertToTypst('\\gamma') + const second = await convertToTypst('\\gamma') + + expect(first).toBe('first') + expect(second).toBe('second') + expect(fetchStub).toHaveBeenCalledTimes(2) + }) + + it('treats same payload as cached even if representing multiple clients in one session', async () => { + const simulatedClients = ['client-a', 'client-b'] + for (const client of simulatedClients) { + const result = await convertToTypst('\\delta') + expect(result).toBe('typst-output') + } + expect(fetchStub).toHaveBeenCalledTimes(1) + }) +}) + diff --git a/test/unit/pandocManager.spec.ts b/test/unit/pandocManager.spec.ts new file mode 100644 index 0000000..892d532 --- /dev/null +++ b/test/unit/pandocManager.spec.ts @@ -0,0 +1,153 @@ +import { describe, it, expect, vi, beforeEach, afterEach, beforeAll } from 'vitest' +import { EventEmitter } from 'events' +import { existsSync, readFileSync } from 'node:fs' + +class MockChildProcess extends EventEmitter { + public stdout = new EventEmitter() + public stderr = new EventEmitter() + public stdin = { + write: vi.fn(), + end: vi.fn() + } + public kill = vi.fn() +} + +type MockedProcess = MockChildProcess + +const mockProcesses: MockedProcess[] = [] + +const spawnMock = vi.fn(() => { + const proc = new MockChildProcess() + mockProcesses.push(proc) + return proc +}) + +vi.mock('child_process', () => ({ + spawn: spawnMock +})) + +const { PandocManager } = await import('../../server/utils/pandocManager') + +function detectDockerEnvironment(): boolean { + if (process.env.FORCE_REAL_PANDOC === '1') { + return true + } + try { + if (existsSync('/.dockerenv')) { + return true + } + } catch {} + try { + const content = readFileSync('/proc/1/cgroup', 'utf8') + if (content.includes('docker') || content.includes('containerd')) { + return true + } + } catch {} + return false +} + +const isDockerRuntime = detectDockerEnvironment() + +function emitSuccess(proc: MockedProcess, output: string) { + proc.stdout.emit('data', Buffer.from(output)) + proc.emit('close', 0) +} + +async function startConversion(manager: PandocManager, latex: string, options?: Parameters[1]) { + const currentIndex = mockProcesses.length + const promise = manager.convert(latex, options) + await Promise.resolve() + const proc = mockProcesses[currentIndex] + if (!proc) { + throw new Error('Mock process did not start') + } + return { promise, proc } +} + +describe('PandocManager', () => { + beforeEach(() => { + mockProcesses.length = 0 + spawnMock.mockClear() + }) + + afterEach(() => { + vi.useRealTimers() + vi.clearAllTimers() + }) + + it('converts latex via pandoc process and trims stdout', async () => { + const manager = new PandocManager(2) + const { promise, proc } = await startConversion(manager, 'x + y') + emitSuccess(proc, ' converted ') + await expect(promise).resolves.toBe('converted') + expect(proc.stdin.write).toHaveBeenCalledWith('x + y') + expect(proc.stdin.end).toHaveBeenCalled() + }) + + it('rejects when pandoc exits with non-zero code', async () => { + const manager = new PandocManager(1) + const { promise, proc } = await startConversion(manager, '\\frac{1}{2}') + proc.stderr.emit('data', Buffer.from('boom')) + proc.emit('close', 1) + await expect(promise).rejects.toThrow(/pandoc conversion failed/i) + }) + + it('rejects when spawn emits error', async () => { + const manager = new PandocManager(1) + const { promise, proc } = await startConversion(manager, 'bad input') + proc.emit('error', new Error('spawn failed')) + await expect(promise).rejects.toThrow(/failed to start pandoc/i) + }) + + it('enforces concurrency limits by deferring queued conversions', async () => { + const manager = new PandocManager(2) + const { promise: p1, proc: proc1 } = await startConversion(manager, 'a') + const { promise: p2, proc: proc2 } = await startConversion(manager, 'b') + const p3Promise = manager.convert('c') + + expect(spawnMock).toHaveBeenCalledTimes(2) + + emitSuccess(proc1, 'a') + await p1 + await Promise.resolve() + expect(spawnMock).toHaveBeenCalledTimes(3) + const proc3 = mockProcesses[2] + if (!proc3) { + throw new Error('Third process was not created') + } + + emitSuccess(proc2, 'b') + emitSuccess(proc3, 'c') + + await expect(p1).resolves.toBe('a') + await expect(p2).resolves.toBe('b') + await expect(p3Promise).resolves.toBe('c') + }) + + it('times out long running conversions', async () => { + vi.useFakeTimers() + const manager = new PandocManager(1) + const { promise } = await startConversion(manager, 'slow', { timeout: 50 }) + vi.advanceTimersByTime(60) + await expect(promise).rejects.toThrow(/timeout/i) + }) +}) + +describe.runIf(isDockerRuntime)('PandocManager real process (docker only)', () => { + let RealPandocManager: typeof PandocManager + + beforeAll(async () => { + vi.resetModules() + vi.doUnmock('child_process') + const mod = await import('../../server/utils/pandocManager') + RealPandocManager = mod.PandocManager + }) + + it('executes real pandoc conversion', async () => { + const manager = new RealPandocManager(1) + const latex = '\\begin{equation}a + b = c\\end{equation}' + const output = await manager.convert(latex, { timeout: 15000 }) + expect(output).toMatch(/a\s*\+\s*b/) + }) +}) + diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..cbc6fc3 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from 'vitest/config' +import { defineVitestProject } from '@nuxt/test-utils/config' + +export default defineConfig({ + test: { + projects: [ + { + test: { + name: 'unit', + include: ['test/{e2e,unit}/*.{test,spec}.ts'], + environment: 'node', + }, + }, + await defineVitestProject({ + test: { + name: 'nuxt', + include: ['test/nuxt/*.{test,spec}.ts'], + environment: 'nuxt', + viteEnvironment: 'nuxt' + } + }), + ], + }, +})