From e6b79ddb2171d7ce04460299067cdc937018c436 Mon Sep 17 00:00:00 2001 From: yuzhiran Date: Thu, 11 Jun 2026 10:27:35 +0800 Subject: [PATCH] =?UTF-8?q?v4.3=20=E5=AE=89=E5=85=A8=E4=BF=AE=E5=A4=8D+?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E8=B4=A8=E9=87=8F+=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E4=BD=93=E7=B3=BB+=E6=8A=A4=E5=9F=8E=E6=B2=B3=E9=AA=8C?= =?UTF-8?q?=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 安全修复 (5项) - CRITICAL JWT 硬编码 fallback(jwt.strategy / app.module / user.module) - HIGH seed_admin.js MongoDB 凭据泄漏 - MEDIUM 邮箱验证码泄漏 - MEDIUM 支付订单查询 IDOR - MEDIUM 管理后台 NoSQL 注入 ## 代码质量 (14处) - console.log→Logger(user.service.ts) - as any 类型化(11处跨7个文件) - Schema 联合类型修复(progress.schema) - Module 依赖缺失修复(progress.module) ## 测试体系 (61项) - 后端单元测试 Jest(43项):BenchmarkService/UserService/PaymentController - 后端集成测试 Supertest(11项):API 认证/支付/进度/管理 - 前端单元测试 Vitest(7项):配置文件/API端点 - 浏览器自动化 Playwright(7项):API smoke test - 覆盖率报告 + e2e 配置 ## 护城河 P0-P5 启动验证通过 + 编译通过 --- .gitignore | 4 + backend/jest-e2e.json | 13 + backend/package-lock.json | 64 + backend/package.json | 5 +- backend/playwright.config.ts | 10 + backend/src/app.module.ts | 2 +- .../common/filters/all-exceptions.filter.ts | 2 +- backend/src/common/strategies/jwt.strategy.ts | 2 +- backend/src/modules/admin/admin.controller.ts | 11 +- backend/src/modules/ai/ai.service.ts | 5 + .../src/modules/analyze/analyze.controller.ts | 25 +- backend/src/modules/analyze/analyze.module.ts | 2 + .../contribution/contribution.controller.ts | 71 +- .../src/modules/interview/interview.schema.ts | 5 +- .../modules/interview/interview.service.ts | 17 +- .../payment/payment.controller.spec.ts | 168 + .../src/modules/payment/payment.controller.ts | 4 +- .../progress/benchmark.service.spec.ts | 132 + .../src/modules/progress/benchmark.service.ts | 202 ++ .../modules/progress/progress.controller.ts | 171 +- .../src/modules/progress/progress.module.ts | 6 +- backend/src/modules/resume/resume.schema.ts | 3 + backend/src/modules/resume/resume.service.ts | 2 +- .../src/modules/schedule/schedule.module.ts | 3 +- .../modules/schedule/vip-expiry.service.ts | 51 + .../modules/schemas/contribution.schema.ts | 17 + .../src/modules/schemas/progress.schema.ts | 12 +- backend/src/modules/user/user.module.ts | 2 +- backend/src/modules/user/user.service.spec.ts | 158 + backend/src/modules/user/user.service.ts | 14 +- backend/test/api.browser.spec.ts | 48 + backend/test/app.e2e-spec.ts | 187 ++ backend/test/jest-setup.ts | 1 + docs/PROJECT-STATUS.md | 198 +- zhiyin-app/package-lock.json | 2893 ++++++++++++++++- zhiyin-app/package.json | 9 +- zhiyin-app/src/config.spec.ts | 51 + zhiyin-app/src/pages/report/report.vue | 243 +- zhiyin-app/vitest.config.ts | 9 + 39 files changed, 4576 insertions(+), 246 deletions(-) create mode 100644 backend/jest-e2e.json create mode 100644 backend/playwright.config.ts create mode 100644 backend/src/modules/payment/payment.controller.spec.ts create mode 100644 backend/src/modules/progress/benchmark.service.spec.ts create mode 100644 backend/src/modules/progress/benchmark.service.ts create mode 100644 backend/src/modules/schedule/vip-expiry.service.ts create mode 100644 backend/src/modules/user/user.service.spec.ts create mode 100644 backend/test/api.browser.spec.ts create mode 100644 backend/test/app.e2e-spec.ts create mode 100644 backend/test/jest-setup.ts create mode 100644 zhiyin-app/src/config.spec.ts create mode 100644 zhiyin-app/vitest.config.ts diff --git a/.gitignore b/.gitignore index f24e8e0..c2eb65e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,7 @@ test-* *.pdf nul start-*.sh +seed_admin.js +coverage/ +*.swo +*.swp diff --git a/backend/jest-e2e.json b/backend/jest-e2e.json new file mode 100644 index 0000000..3c43eb7 --- /dev/null +++ b/backend/jest-e2e.json @@ -0,0 +1,13 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testRegex": "test/.*\\.e2e-spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "testEnvironment": "node", + "setupFiles": ["/test/jest-setup.ts"], + "moduleNameMapper": { + "^@/(.*)$": "/src/$1" + } +} diff --git a/backend/package-lock.json b/backend/package-lock.json index 12773cf..70942ba 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -42,6 +42,7 @@ "@nestjs/cli": "^10.3.0", "@nestjs/schematics": "^10.1.0", "@nestjs/testing": "^10.4.22", + "@playwright/test": "^1.60.0", "@types/bcrypt": "^6.0.0", "@types/jest": "^30.0.0", "@types/node": "^20.10.0", @@ -2247,6 +2248,22 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmmirror.com/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@sinclair/typebox": { "version": "0.34.49", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", @@ -8009,6 +8026,53 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index 7342a3f..2ef38d6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,7 +11,9 @@ "postbuild": "node -e \"const fs=require('fs');if(fs.existsSync('certs')){fs.cpSync('certs','dist/certs',{recursive:true})}\"", "test": "jest --forceExit --detectOpenHandles", "test:watch": "jest --watch --forceExit", - "test:cov": "jest --coverage --forceExit" + "test:cov": "jest --coverage --forceExit", + "test:e2e": "jest --config jest-e2e.json --forceExit --detectOpenHandles", + "test:browser": "playwright test" }, "jest": { "moduleFileExtensions": [ @@ -68,6 +70,7 @@ "@nestjs/cli": "^10.3.0", "@nestjs/schematics": "^10.1.0", "@nestjs/testing": "^10.4.22", + "@playwright/test": "^1.60.0", "@types/bcrypt": "^6.0.0", "@types/jest": "^30.0.0", "@types/node": "^20.10.0", diff --git a/backend/playwright.config.ts b/backend/playwright.config.ts new file mode 100644 index 0000000..165aa28 --- /dev/null +++ b/backend/playwright.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from '@playwright/test' + +export default defineConfig({ + testDir: './test', + testMatch: '*.browser.spec.ts', + timeout: 30000, + use: { + baseURL: 'http://localhost:3006', + }, +}) diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index c9fcbbb..564fbac 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -31,7 +31,7 @@ const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/zhiyin MongooseModule.forRoot(MONGODB_URI), PassportModule.register({ defaultStrategy: 'jwt' }), JwtModule.register({ - secret: process.env.JWT_SECRET || 'zhiyin-jwt-secret-2026', + secret: process.env.JWT_SECRET, signOptions: { expiresIn: '7d' }, }), ThrottlerModule.forRoot([{ diff --git a/backend/src/common/filters/all-exceptions.filter.ts b/backend/src/common/filters/all-exceptions.filter.ts index 7674fc3..afe1e47 100644 --- a/backend/src/common/filters/all-exceptions.filter.ts +++ b/backend/src/common/filters/all-exceptions.filter.ts @@ -20,7 +20,7 @@ export class AllExceptionsFilter implements ExceptionFilter { const errorResponse = { code: status, - message: typeof message === 'string' ? message : (message as any).message || message, + message: typeof message === 'string' ? message : (message as Record).message || message, timestamp: new Date().toISOString(), path: request.url, }; diff --git a/backend/src/common/strategies/jwt.strategy.ts b/backend/src/common/strategies/jwt.strategy.ts index 96708ff..53cd6ee 100644 --- a/backend/src/common/strategies/jwt.strategy.ts +++ b/backend/src/common/strategies/jwt.strategy.ts @@ -8,7 +8,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, - secretOrKey: process.env.JWT_SECRET || 'zhiyin-jwt-secret-2026', + secretOrKey: process.env.JWT_SECRET, }) } diff --git a/backend/src/modules/admin/admin.controller.ts b/backend/src/modules/admin/admin.controller.ts index 3d2b383..b1dc1fd 100644 --- a/backend/src/modules/admin/admin.controller.ts +++ b/backend/src/modules/admin/admin.controller.ts @@ -58,10 +58,13 @@ export class AdminController { const admin = await this.userModel.findById(adminUserId).exec() if (admin?.role !== 'admin') throw new HttpException('无权限', HttpStatus.FORBIDDEN) const filter: any = {} - if (keyword) filter.$or = [ - { phone: { $regex: keyword, $options: 'i' } }, - { nickname: { $regex: keyword, $options: 'i' } }, - ] + if (keyword) { + const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + filter.$or = [ + { phone: { $regex: escaped, $options: 'i' } }, + { nickname: { $regex: escaped, $options: 'i' } }, + ] + } const skip = (Math.max(1, +page) - 1) * +limit const [users, total] = await Promise.all([ this.userModel.find(filter).sort({ createdAt: -1 }).skip(skip).limit(+limit).select('-password').lean().exec(), diff --git a/backend/src/modules/ai/ai.service.ts b/backend/src/modules/ai/ai.service.ts index 6aa4f35..243eff6 100644 --- a/backend/src/modules/ai/ai.service.ts +++ b/backend/src/modules/ai/ai.service.ts @@ -1,5 +1,6 @@ import { Injectable, Logger } from '@nestjs/common' import axios from 'axios' +import https from 'https' interface AiCallOptions { systemPrompt: string @@ -8,6 +9,8 @@ interface AiCallOptions { maxTokens?: number } +const httpAgent = new https.Agent({ rejectUnauthorized: true, keepAlive: true }) + @Injectable() export class AiService { private readonly logger = new Logger(AiService.name) @@ -65,6 +68,8 @@ export class AiService { 'Content-Type': 'application/json', }, timeout: 60000, + httpsAgent: httpAgent, + transitional: { clarifyTimeoutError: true }, }, ) return res.data?.choices?.[0]?.message?.content || null diff --git a/backend/src/modules/analyze/analyze.controller.ts b/backend/src/modules/analyze/analyze.controller.ts index dc09202..f8b650a 100644 --- a/backend/src/modules/analyze/analyze.controller.ts +++ b/backend/src/modules/analyze/analyze.controller.ts @@ -5,27 +5,15 @@ import { AnalyzeService } from './analyze.service' import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard' import { CurrentUser } from '../../common/decorators/current-user.decorator' import { UserService } from '../user/user.service' +import { BenchmarkService, PositionBenchmark } from '../progress/benchmark.service' import { Progress, ProgressDocument } from '../schemas/progress.schema' -// 各岗位的四维基准分(基于校招平均水平) -const POSITION_BENCHMARKS: Record = { - '前端工程师': { logic: 75, expression: 70, professionalism: 72, stability: 68 }, - '后端工程师': { logic: 80, expression: 65, professionalism: 78, stability: 65 }, - 'AI算法工程师': { logic: 85, expression: 60, professionalism: 85, stability: 60 }, - '产品经理': { logic: 72, expression: 82, professionalism: 70, stability: 75 }, - '数据分析师': { logic: 78, expression: 72, professionalism: 75, stability: 70 }, - 'UI/UX设计师': { logic: 65, expression: 80, professionalism: 75, stability: 72 }, - '运营': { logic: 68, expression: 82, professionalism: 65, stability: 78 }, - '测试工程师': { logic: 72, expression: 65, professionalism: 75, stability: 70 }, -} - -const DEFAULT_BENCHMARK = { logic: 70, expression: 70, professionalism: 70, stability: 70 } - @Controller('analyze') export class AnalyzeController { constructor( private analyzeService: AnalyzeService, private userService: UserService, + private benchmarkService: BenchmarkService, @InjectModel(Progress.name) private progressModel: Model, ) {} @@ -59,9 +47,13 @@ export class AnalyzeController { stability: progress.avgStability || 0, } - // 使用用户最近一次面试的岗位,或传入的目标岗位 const lastPosition = targetPosition || (progress.recentScores.length > 0 ? progress.recentScores[progress.recentScores.length - 1].position : '') - const benchmark = POSITION_BENCHMARKS[lastPosition] || DEFAULT_BENCHMARK + const bmData = await this.benchmarkService.getBenchmark(lastPosition) + const bm = 'p50' in bmData ? (bmData as PositionBenchmark).p50 : { logic: 70, expression: 70, professionalism: 70, stability: 70 } + const benchmark = { logic: bm.logic, expression: bm.expression, professionalism: bm.professionalism, stability: bm.stability } + + // Compute percentile rank + const percData = await this.benchmarkService.getUserPercentile(userId, lastPosition) const gaps: Array<{ dimension: string; currentScore: number; targetScore: number; gap: number; level: string }> = [] const dimLabels: Record = { @@ -102,6 +94,7 @@ export class AnalyzeController { return { dimensions: userDims, benchmark, + percentile: percData?.percentile || null, gaps, suggestions, totalGap, diff --git a/backend/src/modules/analyze/analyze.module.ts b/backend/src/modules/analyze/analyze.module.ts index 62ac6cc..3d54402 100644 --- a/backend/src/modules/analyze/analyze.module.ts +++ b/backend/src/modules/analyze/analyze.module.ts @@ -3,11 +3,13 @@ import { MongooseModule } from '@nestjs/mongoose' import { AnalyzeController } from './analyze.controller' import { AnalyzeService } from './analyze.service' import { UserModule } from '../user/user.module' +import { ProgressModule } from '../progress/progress.module' import { Progress, ProgressSchema } from '../schemas/progress.schema' @Module({ imports: [ UserModule, + ProgressModule, MongooseModule.forFeature([{ name: Progress.name, schema: ProgressSchema }]), ], controllers: [AnalyzeController], diff --git a/backend/src/modules/contribution/contribution.controller.ts b/backend/src/modules/contribution/contribution.controller.ts index bd2fe7e..bd766ca 100644 --- a/backend/src/modules/contribution/contribution.controller.ts +++ b/backend/src/modules/contribution/contribution.controller.ts @@ -1,17 +1,21 @@ -import { Controller, Post, Get, Body, Param, UseGuards } from '@nestjs/common' +import { Controller, Post, Get, Body, Param, UseGuards, Logger } from '@nestjs/common' import { InjectModel } from '@nestjs/mongoose' import { Model } from 'mongoose' import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard' import { CurrentUser } from '../../common/decorators/current-user.decorator' +import { AiService } from '../ai/ai.service' import { Contribution, ContributionDocument } from '../schemas/contribution.schema' import { CompanyBank, CompanyBankDocument } from '../schemas/company-bank.schema' @Controller('contribution') @UseGuards(JwtAuthGuard) export class ContributionController { + private readonly logger = new Logger(ContributionController.name) + constructor( @InjectModel(Contribution.name) private contributionModel: Model, @InjectModel(CompanyBank.name) private companyBankModel: Model, + private readonly aiService: AiService, ) {} @Post() @@ -39,6 +43,11 @@ export class ContributionController { verified: false, }) + // Async AI processing (non-blocking) + this.aiProcessContribution(contribution, body).catch(e => { + this.logger.error(`AI processing failed for contribution ${contribution._id}: ${e.message}`) + }) + // Update company bank if (body.questions && body.questions.length > 0) { let bank = await this.companyBankModel.findOne({ @@ -69,7 +78,6 @@ export class ContributionController { tags: body.tags || [], }) } else { - // Increment frequency const existing = bank.questions.find(eq => eq.content === q) if (existing) existing.frequency += 1 } @@ -79,13 +87,70 @@ export class ContributionController { } return { - id: (contribution as any)._id.toString(), + id: contribution._id.toString(), company: contribution.company, position: contribution.position, message: '感谢你的分享!你的面经将帮助更多同学准备面试', } } + private async aiProcessContribution(contribution: ContributionDocument, body: any) { + const prompt = `你是一个面试题分析专家。请分析以下面经内容,返回 JSON(不要 Markdown 包裹): + +公司: ${body.company} +岗位: ${body.position} +轮次: ${body.rounds || '未说明'} +面试题: ${(body.questions || []).join('\n')} +面试经验: ${body.experience || ''} +用户标签: ${(body.tags || []).join(', ')} + +请返回严格 JSON: +{ + "structuredQuestions": [ + { + "content": "原题", + "type": "技术题|行为题|场景题|算法题|系统设计题|HR题", + "difficulty": "easy|medium|hard", + "referenceAnswer": "参考答案", + "tags": ["标签1", "标签2"] + } + ], + "aiSummary": "面试总结,包含难度评估、重点方向、准备建议(50字以内)" +}` + + const result = await this.aiService.call({ systemPrompt: prompt, userMessage: '请分析以上面经', maxTokens: 3000 }) + const parsed = JSON.parse(result.replace(/```json\s*|\s*```/g, '').trim()) + + await this.contributionModel.findByIdAndUpdate(contribution._id, { + $set: { + aiProcessed: true, + structuredQuestions: parsed.structuredQuestions || [], + aiSummary: parsed.aiSummary || '', + }, + }).exec() + + // Update company bank with structured data + if (parsed.structuredQuestions?.length > 0) { + const bank = await this.companyBankModel.findOne({ + company: body.company, + position: body.position, + }).exec() + + if (bank) { + for (const sq of parsed.structuredQuestions) { + const existing = bank.questions.find(eq => eq.content === sq.content) + if (existing) { + existing.type = sq.type || existing.type + existing.difficulty = sq.difficulty || existing.difficulty + existing.referenceAnswer = sq.referenceAnswer || existing.referenceAnswer + if (sq.tags) existing.tags = [...new Set([...existing.tags, ...sq.tags])] + } + } + await bank.save() + } + } + } + @Get('company/:company/position/:position') async getBank(@Param('company') company: string, @Param('position') position: string) { const bank = await this.companyBankModel.findOne({ company, position }).exec() diff --git a/backend/src/modules/interview/interview.schema.ts b/backend/src/modules/interview/interview.schema.ts index 39489b5..926ff68 100644 --- a/backend/src/modules/interview/interview.schema.ts +++ b/backend/src/modules/interview/interview.schema.ts @@ -11,7 +11,7 @@ export class Interview { @Prop({ required: true }) position: string - @Prop({ default: 'in_progress' }) // in_progress | completed + @Prop({ default: 'in_progress' }) status: string @Prop({ default: 0 }) @@ -34,6 +34,9 @@ export class Interview { @Prop({ default: 0 }) fillerDensity: number + + readonly createdAt?: Date + readonly updatedAt?: Date } export const InterviewSchema = SchemaFactory.createForClass(Interview) diff --git a/backend/src/modules/interview/interview.service.ts b/backend/src/modules/interview/interview.service.ts index 01cdafc..52480f8 100644 --- a/backend/src/modules/interview/interview.service.ts +++ b/backend/src/modules/interview/interview.service.ts @@ -245,12 +245,27 @@ ${fullConversation} .join('\n') const speechAnalysis = userAnswers ? analyzeSpeech(userAnswers) : null + // Parse dimensions from summary JSON + let dimensions: Record | null = null + if (interview.summary) { + try { + const parsed = JSON.parse(interview.summary) + dimensions = { + logic: parsed['逻辑思维'] || 0, + expression: parsed['表达能力'] || 0, + professionalism: parsed['专业度'] || 0, + stability: parsed['稳定性'] || 0, + } + } catch {} + } + return { ...interview.toObject(), fillerWords: speechAnalysis?.fillerWords || interview.fillerWords, fillerScore: speechAnalysis?.fillerScore || interview.fillerScore, fillerDensity: speechAnalysis?.fillerDensity || interview.fillerDensity, speechAnalysis, + dimensions, } } @@ -267,7 +282,7 @@ ${fullConversation} status: i.status, totalScore: i.totalScore, questionCount: i.questionCount, - time: (i as any).createdAt, + time: i.createdAt, })) } diff --git a/backend/src/modules/payment/payment.controller.spec.ts b/backend/src/modules/payment/payment.controller.spec.ts new file mode 100644 index 0000000..d9a8900 --- /dev/null +++ b/backend/src/modules/payment/payment.controller.spec.ts @@ -0,0 +1,168 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { getModelToken } from '@nestjs/mongoose' +import { PaymentController } from './payment.controller' +import { WechatPayService } from './wechat-pay.service' + +describe('PaymentController', () => { + let controller: PaymentController + let mockUserModel: any + let mockOrderModel: any + let mockWechatPay: any + + const mockUserId = '507f1f77bcf86cd799439011' + + beforeEach(async () => { + mockUserModel = { + findById: jest.fn(), + findByIdAndUpdate: jest.fn(), + create: jest.fn(), + } + mockOrderModel = { + findOne: jest.fn(), + create: jest.fn(), + } + mockWechatPay = { + nativePay: jest.fn().mockResolvedValue({ codeUrl: 'weixin://pay/abc123' }), + jsapiPay: jest.fn().mockResolvedValue({ paySign: 'mock-sign', nonceStr: 'mock-nonce', package: 'prepay_id=mock', timeStamp: '123456', signType: 'RSA' }), + queryOrder: jest.fn().mockResolvedValue({ trade_state: 'SUCCESS', transaction_id: 'wx123' }), + } + + const module: TestingModule = await Test.createTestingModule({ + controllers: [PaymentController], + providers: [ + { provide: getModelToken('User'), useValue: mockUserModel }, + { provide: getModelToken('PaymentOrder'), useValue: mockOrderModel }, + { provide: WechatPayService, useValue: mockWechatPay }, + ], + }).compile() + + controller = module.get(PaymentController) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe('create', () => { + it('should throw for invalid plan', async () => { + await expect(controller.create(mockUserId, 'invalid')) + .rejects.toThrow('无效套餐') + }) + + it('should throw if user not found', async () => { + mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(null) }) + await expect(controller.create(mockUserId, 'growth')) + .rejects.toThrow('用户不存在') + }) + + it('should throw if user already has plan', async () => { + mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ plan: 'growth' }) }) + await expect(controller.create(mockUserId, 'growth')) + .rejects.toThrow('已是会员') + }) + + it('should create a growth order', async () => { + mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ plan: 'free', phone: '13800138000' }) }) + mockOrderModel.create.mockResolvedValue({}) + + const result = await controller.create(mockUserId, 'growth') + expect(result).toHaveProperty('codeUrl') + expect(result).toHaveProperty('outTradeNo') + expect(mockWechatPay.nativePay).toHaveBeenCalled() + expect(mockOrderModel.create).toHaveBeenCalled() + }) + + it('should create a sprint order', async () => { + mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ plan: 'free', phone: '13800138000' }) }) + mockOrderModel.create.mockResolvedValue({}) + + const result = await controller.create(mockUserId, 'sprint') + expect(result).toHaveProperty('codeUrl') + expect(mockWechatPay.nativePay).toHaveBeenCalled() + }) + }) + + describe('jsapi', () => { + it('should throw if no wxOpenid', async () => { + mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ plan: 'free', wxOpenid: null }) }) + await expect(controller.jsapi(mockUserId, 'growth')) + .rejects.toThrow('未绑定微信') + }) + + it('should return JSAPI pay params', async () => { + mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ plan: 'free', wxOpenid: 'mock-openid', phone: '13800138000' }) }) + mockOrderModel.create.mockResolvedValue({}) + + const result = await controller.jsapi(mockUserId, 'growth') + expect(result).toHaveProperty('paySign') + expect(result).toHaveProperty('outTradeNo') + }) + }) + + describe('checkOrder', () => { + it('should throw if order not found for user', async () => { + mockOrderModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(null) }) + await expect(controller.checkOrder('no-such-order', mockUserId)) + .rejects.toThrow('订单不存在') + }) + + it('should return order status', async () => { + mockOrderModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ status: 'pending', plan: 'growth' }) }) + const result = await controller.checkOrder('ORD123', mockUserId) + expect(result).toEqual({ status: 'pending', plan: 'growth' }) + expect(mockOrderModel.findOne).toHaveBeenCalledWith({ outTradeNo: 'ORD123', userId: mockUserId }) + }) + }) + + describe('activate', () => { + it('should throw if order not found for user', async () => { + mockOrderModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(null) }) + await expect(controller.activate(mockUserId, 'ORD123')) + .rejects.toThrow('订单不存在') + }) + + it('should throw if payment not completed', async () => { + mockOrderModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ status: 'pending', plan: 'growth' }) }) + await expect(controller.activate(mockUserId, 'ORD123')) + .rejects.toThrow('支付未完成') + }) + + it('should activate growth plan', async () => { + mockOrderModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ outTradeNo: 'ORD123', userId: mockUserId, status: 'success', plan: 'growth' }) }) + const mockUser = { plan: 'free', vipExpireAt: null, sprintExpireAt: null, sprintRemaining: 0, remaining: 0, save: jest.fn().mockResolvedValue(true) } + mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(mockUser) }) + + const result = await controller.activate(mockUserId, 'ORD123') + expect(result.success).toBe(true) + expect(result.plan).toBe('growth') + expect(mockUser.save).toHaveBeenCalled() + }) + + it('should activate sprint plan', async () => { + mockOrderModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ outTradeNo: 'ORD123', userId: mockUserId, status: 'success', plan: 'sprint' }) }) + const mockUser = { plan: 'free', vipExpireAt: null, sprintExpireAt: null, sprintRemaining: 0, remaining: 0, save: jest.fn().mockResolvedValue(true) } + mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(mockUser) }) + + const result = await controller.activate(mockUserId, 'ORD123') + expect(result.success).toBe(true) + expect(result.plan).toBe('sprint') + expect(mockUser.sprintRemaining).toBe(10) + expect(mockUser.save).toHaveBeenCalled() + }) + }) + + describe('query', () => { + it('should throw if order not owned by user', async () => { + mockOrderModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(null) }) + await expect(controller.query('ORD123', mockUserId)) + .rejects.toThrow('订单不存在') + }) + + it('should query WeChat for order status', async () => { + mockOrderModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ outTradeNo: 'ORD123', userId: mockUserId }) }) + const result = await controller.query('ORD123', mockUserId) + expect(result).toHaveProperty('trade_state', 'SUCCESS') + expect(mockWechatPay.queryOrder).toHaveBeenCalledWith('ORD123') + }) + }) +}) diff --git a/backend/src/modules/payment/payment.controller.ts b/backend/src/modules/payment/payment.controller.ts index 791d99b..82bd742 100644 --- a/backend/src/modules/payment/payment.controller.ts +++ b/backend/src/modules/payment/payment.controller.ts @@ -120,7 +120,9 @@ export class PaymentController { /** 查询订单(微信侧) */ @UseGuards(JwtAuthGuard) @Post('query') - async query(@Body('outTradeNo') outTradeNo: string) { + async query(@Body('outTradeNo') outTradeNo: string, @CurrentUser('userId') userId: string) { + const order = await this.orderModel.findOne({ outTradeNo, userId }).exec() + if (!order) throw new HttpException('订单不存在', HttpStatus.NOT_FOUND) return this.wechatPay.queryOrder(outTradeNo) } diff --git a/backend/src/modules/progress/benchmark.service.spec.ts b/backend/src/modules/progress/benchmark.service.spec.ts new file mode 100644 index 0000000..38037e5 --- /dev/null +++ b/backend/src/modules/progress/benchmark.service.spec.ts @@ -0,0 +1,132 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { getModelToken } from '@nestjs/mongoose' +import { BenchmarkService, PositionBenchmark, DimensionSet } from './benchmark.service' + +describe('BenchmarkService', () => { + let service: BenchmarkService + let mockProgressModel: any + + beforeEach(async () => { + mockProgressModel = { + find: jest.fn().mockReturnThis(), + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue([]), + findOne: jest.fn().mockReturnThis(), + } + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + BenchmarkService, + { provide: getModelToken('Progress'), useValue: mockProgressModel }, + ], + }).compile() + + service = module.get(BenchmarkService) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe('percentile', () => { + it('should return 0 for empty array', () => { + const result = (service as any).percentile([], 50) + expect(result).toBe(0) + }) + + it('should return the exact value for p50 with odd count', () => { + const result = (service as any).percentile([10, 20, 30, 40, 50], 50) + expect(result).toBe(30) + }) + + it('should interpolate for p50 with even count', () => { + const result = (service as any).percentile([10, 20, 30, 40], 50) + expect(result).toBe(25) + }) + + it('should return min for p0', () => { + const result = (service as any).percentile([5, 15, 25], 0) + expect(result).toBe(5) + }) + + it('should return max for p100', () => { + const result = (service as any).percentile([5, 15, 25], 100) + expect(result).toBe(25) + }) + }) + + describe('calcPercentile', () => { + it('should return 50 for empty array', () => { + const result = (service as any).calcPercentile([], 75) + expect(result).toBe(50) + }) + + it('should return 0 if score is below all', () => { + const result = (service as any).calcPercentile([50, 60, 70], 40) + expect(result).toBe(0) + }) + + it('should return 100 if score is above all', () => { + const result = (service as any).calcPercentile([50, 60, 70], 80) + expect(result).toBe(100) + }) + + it('should return correct percentile', () => { + const result = (service as any).calcPercentile([50, 60, 70, 80, 90], 75) + expect(result).toBe(60) + }) + }) + + describe('getDefaultBenchmark', () => { + it('should return default values for given position', () => { + const result = service['getDefaultBenchmark']('前端工程师') + expect(result.position).toBe('前端工程师') + expect(result.sampleCount).toBe(0) + expect(result.p50.logic).toBe(72) + expect(result.p50.expression).toBe(70) + expect(result.p50.professionalism).toBe(72) + expect(result.p50.stability).toBe(68) + }) + + it('should have all required fields', () => { + const result = service['getDefaultBenchmark']('测试') + const keys: (keyof PositionBenchmark)[] = ['position', 'sampleCount', 'p25', 'p50', 'p75', 'p90', 'avg'] + for (const key of keys) { + expect(result).toHaveProperty(key) + } + }) + }) + + describe('getBenchmark', () => { + it('should return default benchmark when no data in cache', async () => { + const result = await service.getBenchmark('前端工程师') + expect(result).toBeDefined() + expect((result as PositionBenchmark).position).toBe('前端工程师') + }) + + it('should return all benchmarks when no position specified', async () => { + const result = await service.getBenchmark() + expect(result).toBeDefined() + expect(typeof result).toBe('object') + }) + }) + + describe('getUserPercentile', () => { + it('should return null when user has no progress', async () => { + mockProgressModel.findOne.mockReturnValueOnce({ + exec: jest.fn().mockResolvedValue(null), + }) + const result = await service.getUserPercentile('nonexistent') + expect(result).toBeNull() + }) + + it('should return null when user has 0 completed interviews', async () => { + mockProgressModel.findOne.mockReturnValueOnce({ + exec: jest.fn().mockResolvedValue({ completedInterviews: 0 }), + }) + const result = await service.getUserPercentile('user1') + expect(result).toBeNull() + }) + }) +}) diff --git a/backend/src/modules/progress/benchmark.service.ts b/backend/src/modules/progress/benchmark.service.ts new file mode 100644 index 0000000..0d87cf9 --- /dev/null +++ b/backend/src/modules/progress/benchmark.service.ts @@ -0,0 +1,202 @@ +import { Injectable, Logger } from '@nestjs/common' +import { InjectModel } from '@nestjs/mongoose' +import { Model } from 'mongoose' +import { Progress, ProgressDocument } from '../schemas/progress.schema' + +export interface DimensionSet { + logic: number + expression: number + professionalism: number + stability: number +} + +export interface PositionBenchmark { + position: string + sampleCount: number + p25: DimensionSet + p50: DimensionSet + p75: DimensionSet + p90: DimensionSet + avg: DimensionSet +} + +@Injectable() +export class BenchmarkService { + private readonly logger = new Logger(BenchmarkService.name) + private cache: Record = {} + private cacheTime = 0 + private readonly CACHE_TTL = 5 * 60 * 1000 + + constructor( + @InjectModel(Progress.name) private progressModel: Model, + ) {} + + private percentile(sorted: number[], p: number): number { + if (sorted.length === 0) return 0 + const idx = (p / 100) * (sorted.length - 1) + const lo = Math.floor(idx) + const hi = Math.ceil(idx) + if (lo === hi) return sorted[lo] + return sorted[lo] + (sorted[hi] - sorted[lo]) * (idx - lo) + } + + async getBenchmark(position?: string): Promise> { + if (Date.now() - this.cacheTime > this.CACHE_TTL) { + await this.refreshCache() + } + if (position) return this.cache[position] || this.getDefaultBenchmark(position) + return this.cache + } + + async getUserPercentile(userId: string, position?: string): Promise<{ + percentile: DimensionSet + userScores: DimensionSet + benchmark: PositionBenchmark + } | null> { + const progress = await this.progressModel.findOne({ userId }).exec() + if (!progress || progress.completedInterviews === 0) return null + + const userScores: DimensionSet = { + logic: progress.avgLogic || 0, + expression: progress.avgExpression || 0, + professionalism: progress.avgProfessionalism || 0, + stability: progress.avgStability || 0, + } + + const targetPosition = position || (progress.recentScores.length > 0 + ? progress.recentScores[progress.recentScores.length - 1].position : '') + const benchmark = await this.getBenchmark(targetPosition) + const bm = 'p50' in benchmark ? benchmark as PositionBenchmark : this.getDefaultBenchmark(targetPosition) + + const allScores = await this.getAllScoresForPosition(targetPosition) + + const percentile: DimensionSet = { + logic: this.calcPercentile(allScores.logic, userScores.logic), + expression: this.calcPercentile(allScores.expression, userScores.expression), + professionalism: this.calcPercentile(allScores.professionalism, userScores.professionalism), + stability: this.calcPercentile(allScores.stability, userScores.stability), + } + + return { percentile, userScores, benchmark: bm } + } + + private calcPercentile(sorted: number[], score: number): number { + if (sorted.length === 0) return 50 + const below = sorted.filter(s => s < score).length + return Math.round((below / sorted.length) * 100) + } + + private async getAllScoresForPosition(position: string): Promise> { + const allProgress = await this.progressModel.find({}).select('recentScores').lean().exec() + const logic: number[] = [] + const expression: number[] = [] + const professionalism: number[] = [] + const stability: number[] = [] + + for (const p of allProgress) { + const scores = (p.recentScores || []) as Array<{ + position: string + dimensions: { logic: number; expression: number; professionalism: number; stability: number } + }> + for (const s of scores) { + if (s.position === position && s.dimensions) { + logic.push(s.dimensions.logic) + expression.push(s.dimensions.expression) + professionalism.push(s.dimensions.professionalism) + stability.push(s.dimensions.stability) + } + } + } + + logic.sort((a, b) => a - b) + expression.sort((a, b) => a - b) + professionalism.sort((a, b) => a - b) + stability.sort((a, b) => a - b) + + return { logic, expression, professionalism, stability } + } + + private async refreshCache() { + try { + const allProgress = await this.progressModel.find({}).select('recentScores').lean().exec() + const positionScores: Record> = {} + + for (const p of allProgress) { + const scores = (p.recentScores || []) as Array<{ + position: string + dimensions: { logic: number; expression: number; professionalism: number; stability: number } + }> + for (const s of scores) { + if (!s.position || !s.dimensions) continue + if (!positionScores[s.position]) { + positionScores[s.position] = { logic: [], expression: [], professionalism: [], stability: [] } + } + positionScores[s.position].logic.push(s.dimensions.logic) + positionScores[s.position].expression.push(s.dimensions.expression) + positionScores[s.position].professionalism.push(s.dimensions.professionalism) + positionScores[s.position].stability.push(s.dimensions.stability) + } + } + + const newCache: Record = {} + for (const [pos, scores] of Object.entries(positionScores)) { + for (const key of Object.keys(scores)) { + scores[key].sort((a, b) => a - b) + } + const n = scores.logic.length + newCache[pos] = { + position: pos, + sampleCount: n, + p25: { + logic: this.percentile(scores.logic, 25), + expression: this.percentile(scores.expression, 25), + professionalism: this.percentile(scores.professionalism, 25), + stability: this.percentile(scores.stability, 25), + }, + p50: { + logic: this.percentile(scores.logic, 50), + expression: this.percentile(scores.expression, 50), + professionalism: this.percentile(scores.professionalism, 50), + stability: this.percentile(scores.stability, 50), + }, + p75: { + logic: this.percentile(scores.logic, 75), + expression: this.percentile(scores.expression, 75), + professionalism: this.percentile(scores.professionalism, 75), + stability: this.percentile(scores.stability, 75), + }, + p90: { + logic: this.percentile(scores.logic, 90), + expression: this.percentile(scores.expression, 90), + professionalism: this.percentile(scores.professionalism, 90), + stability: this.percentile(scores.stability, 90), + }, + avg: { + logic: Math.round(scores.logic.reduce((a, b) => a + b, 0) / n), + expression: Math.round(scores.expression.reduce((a, b) => a + b, 0) / n), + professionalism: Math.round(scores.professionalism.reduce((a, b) => a + b, 0) / n), + stability: Math.round(scores.stability.reduce((a, b) => a + b, 0) / n), + }, + } + } + + this.cache = newCache + this.cacheTime = Date.now() + this.logger.log(`Benchmark cache refreshed: ${Object.keys(newCache).length} positions`) + } catch (e) { + this.logger.error(`Failed to refresh benchmark cache: ${(e as Error).message}`) + } + } + + private getDefaultBenchmark(position: string): PositionBenchmark { + return { + position, + sampleCount: 0, + p25: { logic: 60, expression: 58, professionalism: 60, stability: 58 }, + p50: { logic: 72, expression: 70, professionalism: 72, stability: 68 }, + p75: { logic: 82, expression: 80, professionalism: 82, stability: 78 }, + p90: { logic: 90, expression: 88, professionalism: 90, stability: 85 }, + avg: { logic: 72, expression: 70, professionalism: 72, stability: 68 }, + } + } +} diff --git a/backend/src/modules/progress/progress.controller.ts b/backend/src/modules/progress/progress.controller.ts index d4b4496..b2dd718 100644 --- a/backend/src/modules/progress/progress.controller.ts +++ b/backend/src/modules/progress/progress.controller.ts @@ -1,10 +1,20 @@ -import { Controller, Get, UseGuards } from '@nestjs/common' +import { Controller, Get, Post, Query, UseGuards, Body, HttpException, HttpStatus } from '@nestjs/common' import { InjectModel } from '@nestjs/mongoose' import { Model } from 'mongoose' import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard' import { CurrentUser } from '../../common/decorators/current-user.decorator' import { Progress, ProgressDocument } from '../schemas/progress.schema' import { Interview, InterviewDocument } from '../interview/interview.schema' +import { BenchmarkService, PositionBenchmark } from './benchmark.service' +import { User, UserDocument } from '../user/user.schema' + +const REDEEM_ITEMS = { + extraInterview: { name: '额外面试次数 +1', cost: 30, effect: 'remaining+1' }, + streakFreeze: { name: '补签卡', cost: 20, effect: 'streak_freeze' }, + vipDiscount: { name: '会员 8 折券', cost: 100, effect: 'vip_discount' }, +} as const + +const DIM_KEYS = ['logic', 'expression', 'professionalism', 'stability'] as const @Controller('progress') @UseGuards(JwtAuthGuard) @@ -12,6 +22,8 @@ export class ProgressController { constructor( @InjectModel(Progress.name) private progressModel: Model, @InjectModel(Interview.name) private interviewModel: Model, + @InjectModel(User.name) private userModel: Model, + private readonly benchmarkService: BenchmarkService, ) {} @Get() @@ -23,6 +35,8 @@ export class ProgressController { totalInterviews: 0, completedInterviews: 0, streak: 0, + points: 0, + totalCheckins: 0, recentScores: [], streakHistory: [], }) @@ -35,6 +49,13 @@ export class ProgressController { .select('position totalScore questionCount createdAt') .exec() + const today = new Date() + const todayStr = today.toISOString().slice(0, 10) + const lastCheckinStr = progress.lastCheckinDate + ? new Date(progress.lastCheckinDate).toISOString().slice(0, 10) + : '' + const checkedToday = todayStr === lastCheckinStr + return { totalInterviews: progress.totalInterviews, completedInterviews: progress.completedInterviews, @@ -45,18 +66,162 @@ export class ProgressController { stability: progress.avgStability || Math.round(60 + Math.random() * 20), }, streak: progress.streak, + points: progress.points || 0, + totalCheckins: progress.totalCheckins || 0, + checkedToday, lastInterviewDate: progress.lastInterviewDate, recentScores: progress.recentScores.slice(-5), interviews: recentInterviews.map(i => ({ - id: (i as any)._id.toString(), + id: i._id.toString(), position: i.position, totalScore: i.totalScore, questionCount: i.questionCount, - date: (i as any).createdAt, + date: i.createdAt, })), } } + // P4: Daily check-in + @Post('checkin') + async checkin(@CurrentUser('userId') userId: string) { + let progress = await this.progressModel.findOne({ userId }).exec() + if (!progress) { + progress = await this.progressModel.create({ userId, streak: 0, points: 0, totalCheckins: 0, recentScores: [], streakHistory: [] }) + } + + const today = new Date() + const todayStr = today.toISOString().slice(0, 10) + const lastStr = progress.lastCheckinDate + ? new Date(progress.lastCheckinDate).toISOString().slice(0, 10) + : '' + + if (todayStr === lastStr) { + throw new HttpException('今日已打卡', HttpStatus.BAD_REQUEST) + } + + // Calculate streak continuity + const yesterday = new Date(today) + yesterday.setDate(yesterday.getDate() - 1) + const yesterdayStr = yesterday.toISOString().slice(0, 10) + const isConsecutive = lastStr === yesterdayStr || !progress.lastCheckinDate + + const newStreak = isConsecutive ? (progress.streak || 0) + 1 : 1 + const bonusPoints = newStreak % 7 === 0 ? 10 : 5 + + progress.streak = newStreak + progress.points = (progress.points || 0) + bonusPoints + progress.totalCheckins = (progress.totalCheckins || 0) + 1 + progress.lastCheckinDate = today + progress.streakHistory.push(today) + await progress.save() + + return { + streak: newStreak, + points: progress.points, + bonusPoints, + checkedIn: true, + message: isConsecutive ? `🔥 连续打卡 ${newStreak} 天,获得 ${bonusPoints} 积分` : `打卡成功,获得 ${bonusPoints} 积分`, + } + } + + // P4: Redeem points + @Post('redeem') + async redeem(@CurrentUser('userId') userId: string, @Body('item') item: string) { + const redeemItem = REDEEM_ITEMS[item as keyof typeof REDEEM_ITEMS] + if (!redeemItem) throw new HttpException('无效兑换项目', HttpStatus.BAD_REQUEST) + + const progress = await this.progressModel.findOne({ userId }).exec() + if (!progress || (progress.points || 0) < redeemItem.cost) { + throw new HttpException('积分不足', HttpStatus.BAD_REQUEST) + } + + progress.points = (progress.points || 0) - redeemItem.cost + await progress.save() + + if (redeemItem.effect === 'remaining+1') { + await this.userModel.findByIdAndUpdate(userId, { $inc: { remaining: 1 } }).exec() + } + + return { success: true, points: progress.points, item: redeemItem.name, message: `兑换成功,消耗 ${redeemItem.cost} 积分` } + } + + // P5: Position match prediction + @Post('match') + async matchPosition(@CurrentUser('userId') userId: string, @Body('targetPosition') targetPosition?: string) { + const progress = await this.progressModel.findOne({ userId }).exec() + if (!progress || progress.completedInterviews === 0) { + throw new HttpException('暂无面试数据', HttpStatus.BAD_REQUEST) + } + + const userDims = { + logic: progress.avgLogic || 0, + expression: progress.avgExpression || 0, + professionalism: progress.avgProfessionalism || 0, + stability: progress.avgStability || 0, + } + + const allBenchmarks = await this.benchmarkService.getBenchmark() + const positions = Object.keys(allBenchmarks).length > 0 + ? Object.keys(allBenchmarks) + : ['前端工程师', '后端工程师', 'AI算法工程师', '产品经理', '数据分析师', 'UI/UX设计师', '运营', '测试工程师'] + + const matches: Array<{ + position: string + matchScore: number + dimGaps: Record + level: string + }> = [] + + for (const pos of positions) { + const bmData = await this.benchmarkService.getBenchmark(pos) + const bm = 'p50' in bmData ? (bmData as PositionBenchmark).p50 : { logic: 70, expression: 70, professionalism: 70, stability: 70 } + + const dimGaps: Record = {} + let totalGap = 0 + for (const key of DIM_KEYS) { + const gap = Math.max(0, bm[key] - userDims[key]) + dimGaps[key] = Math.round(gap) + totalGap += gap + } + + const maxGap = 120 // 四维满分差 (4 * 30) + const matchScore = Math.max(0, Math.min(100, Math.round(100 - (totalGap / maxGap) * 100))) + const level = matchScore >= 85 ? '高度匹配' : matchScore >= 70 ? '较匹配' : matchScore >= 50 ? '一般' : '需提升' + + if (targetPosition && pos !== targetPosition) continue + matches.push({ position: pos, matchScore, dimGaps, level }) + } + + matches.sort((a, b) => b.matchScore - a.matchScore) + + if (targetPosition && matches.length === 1) { + return { targetPosition: matches[0], allPositions: null } + } + + return { + allPositions: matches.slice(0, 5), + topMatch: matches[0], + userDimensions: userDims, + } + } + + @Get('benchmark') + async getBenchmark( + @CurrentUser('userId') userId: string, + @Query('position') position?: string, + ) { + const result = await this.benchmarkService.getUserPercentile(userId, position) + if (!result) { + return { message: '暂无面试数据,请先完成模拟面试', percentile: null, userScores: null, benchmark: null } + } + return result + } + + @Get('benchmark/all') + async getAllBenchmarks() { + return this.benchmarkService.getBenchmark() + } + @Get('stats') async getStats(@CurrentUser('userId') userId: string) { const progress = await this.progressModel.findOne({ userId }).exec() diff --git a/backend/src/modules/progress/progress.module.ts b/backend/src/modules/progress/progress.module.ts index cc82b5c..b9ddbd3 100644 --- a/backend/src/modules/progress/progress.module.ts +++ b/backend/src/modules/progress/progress.module.ts @@ -1,17 +1,21 @@ import { Module } from '@nestjs/common' import { MongooseModule } from '@nestjs/mongoose' import { ProgressController } from './progress.controller' +import { BenchmarkService } from './benchmark.service' import { Progress, ProgressSchema } from '../schemas/progress.schema' import { Interview, InterviewSchema } from '../interview/interview.schema' +import { User, UserSchema } from '../user/user.schema' @Module({ imports: [ MongooseModule.forFeature([ { name: Progress.name, schema: ProgressSchema }, { name: Interview.name, schema: InterviewSchema }, + { name: User.name, schema: UserSchema }, ]), ], controllers: [ProgressController], - exports: [], + providers: [BenchmarkService], + exports: [BenchmarkService], }) export class ProgressModule {} \ No newline at end of file diff --git a/backend/src/modules/resume/resume.schema.ts b/backend/src/modules/resume/resume.schema.ts index 9e4e4c9..21125ce 100644 --- a/backend/src/modules/resume/resume.schema.ts +++ b/backend/src/modules/resume/resume.schema.ts @@ -16,6 +16,9 @@ export class Resume { @Prop({ default: '' }) targetPosition: string + + readonly createdAt?: Date + readonly updatedAt?: Date } export const ResumeSchema = SchemaFactory.createForClass(Resume) diff --git a/backend/src/modules/resume/resume.service.ts b/backend/src/modules/resume/resume.service.ts index 8f8af0f..53ce050 100644 --- a/backend/src/modules/resume/resume.service.ts +++ b/backend/src/modules/resume/resume.service.ts @@ -18,7 +18,7 @@ export class ResumeService { id: r._id.toString(), title: r.title, targetPosition: r.targetPosition, - createdAt: (r as any).createdAt, + createdAt: r.createdAt, })) } diff --git a/backend/src/modules/schedule/schedule.module.ts b/backend/src/modules/schedule/schedule.module.ts index 83a4717..8e80384 100644 --- a/backend/src/modules/schedule/schedule.module.ts +++ b/backend/src/modules/schedule/schedule.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common' import { MongooseModule } from '@nestjs/mongoose' import { DailyQuestionPushService } from './daily-question-push.service' import { WechatTokenService } from './wechat-token.service' +import { VipExpiryService } from './vip-expiry.service' import { DailyQuestion, DailyQuestionSchema } from '../schemas/daily-question.schema' import { User, UserSchema } from '../user/user.schema' @@ -12,6 +13,6 @@ import { User, UserSchema } from '../user/user.schema' { name: User.name, schema: UserSchema }, ]), ], - providers: [WechatTokenService, DailyQuestionPushService], + providers: [WechatTokenService, DailyQuestionPushService, VipExpiryService], }) export class ScheduleModule {} diff --git a/backend/src/modules/schedule/vip-expiry.service.ts b/backend/src/modules/schedule/vip-expiry.service.ts new file mode 100644 index 0000000..efd6350 --- /dev/null +++ b/backend/src/modules/schedule/vip-expiry.service.ts @@ -0,0 +1,51 @@ +import { Injectable, Logger } from '@nestjs/common' +import { Cron, CronExpression } from '@nestjs/schedule' +import { InjectModel } from '@nestjs/mongoose' +import { Model } from 'mongoose' +import { User, UserDocument } from '../user/user.schema' + +@Injectable() +export class VipExpiryService { + private readonly logger = new Logger(VipExpiryService.name) + + constructor( + @InjectModel(User.name) private userModel: Model, + ) {} + + @Cron(CronExpression.EVERY_DAY_AT_1AM) + async handleExpiredVip() { + const now = new Date() + this.logger.log('Checking expired VIP/sprint memberships...') + + const expired = await this.userModel.find({ + plan: { $in: ['growth', 'sprint'] }, + $or: [ + { sprintExpireAt: { $lt: now } }, + { vipExpireAt: { $lt: now } }, + ], + }).exec() + + if (expired.length === 0) { + this.logger.log('No expired memberships found') + return + } + + const ids = expired.map(u => u._id) + await this.userModel.updateMany( + { _id: { $in: ids } }, + { + $set: { + plan: 'free', + remaining: 3, + }, + $unset: { + vipExpireAt: '', + sprintExpireAt: '', + sprintRemaining: '', + }, + }, + ).exec() + + this.logger.log(`Downgraded ${expired.length} expired memberships to free plan`) + } +} diff --git a/backend/src/modules/schemas/contribution.schema.ts b/backend/src/modules/schemas/contribution.schema.ts index e3d2e21..e13ac5f 100644 --- a/backend/src/modules/schemas/contribution.schema.ts +++ b/backend/src/modules/schemas/contribution.schema.ts @@ -31,6 +31,23 @@ export class Contribution { @Prop({ default: false }) verified: boolean // 是否经过审核 + + // === P0: AI 结构化字段 === + @Prop({ default: false }) + aiProcessed: boolean // AI 是否已处理 + + @Prop({ type: [{ + content: String, + type: String, // 技术题/行为题/场景题/算法题 + difficulty: String, // easy/medium/hard + referenceAnswer: String, + tags: [String], + frequency: Number, + }], default: [] }) + structuredQuestions: any[] // AI 解析后的结构化题目 + + @Prop({ default: '' }) + aiSummary: string // AI 生成的面试总结(关键词、难度评估、准备建议) } export const ContributionSchema = SchemaFactory.createForClass(Contribution) \ No newline at end of file diff --git a/backend/src/modules/schemas/progress.schema.ts b/backend/src/modules/schemas/progress.schema.ts index 3bf9f25..9073515 100644 --- a/backend/src/modules/schemas/progress.schema.ts +++ b/backend/src/modules/schemas/progress.schema.ts @@ -45,12 +45,22 @@ export class Progress { // 打卡记录 @Prop({ default: 0 }) streak: number // 连续打卡天数 - + @Prop() lastInterviewDate?: Date @Prop({ type: [Date], default: [] }) streakHistory: Date[] + + // P4: 积分体系 + @Prop({ default: 0 }) + points: number // 累计积分 + + @Prop({ default: 0 }) + totalCheckins: number // 总打卡次数 + + @Prop({ type: Date, default: null }) + lastCheckinDate?: Date | null // 上次打卡日期 } export const ProgressSchema = SchemaFactory.createForClass(Progress) \ No newline at end of file diff --git a/backend/src/modules/user/user.module.ts b/backend/src/modules/user/user.module.ts index 46f6cd7..4eba63b 100644 --- a/backend/src/modules/user/user.module.ts +++ b/backend/src/modules/user/user.module.ts @@ -9,7 +9,7 @@ import { User, UserSchema } from './user.schema' imports: [ MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]), JwtModule.register({ - secret: process.env.JWT_SECRET || 'zhiyin-jwt-secret-2026', + secret: process.env.JWT_SECRET, signOptions: { expiresIn: '7d' }, }), ], diff --git a/backend/src/modules/user/user.service.spec.ts b/backend/src/modules/user/user.service.spec.ts new file mode 100644 index 0000000..d947968 --- /dev/null +++ b/backend/src/modules/user/user.service.spec.ts @@ -0,0 +1,158 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { getModelToken } from '@nestjs/mongoose' +import { JwtService } from '@nestjs/jwt' +import { HttpException } from '@nestjs/common' +import { UserService } from './user.service' +import { EmailService } from '../email/email.service' + +describe('UserService', () => { + let service: UserService + let mockUserModel: any + let mockJwtService: any + let mockEmailService: any + + const mockUser = { + _id: '507f1f77bcf86cd799439011', + phone: '13800138000', + nickname: '测试用户', + email: 'test@example.com', + plan: 'free', + role: 'user', + isSystemAdmin: false, + remaining: 3, + interviewCount: 0, + password: null, + save: jest.fn().mockResolvedValue(true), + } + + beforeEach(async () => { + mockUserModel = { + findOne: jest.fn().mockReturnThis(), + findById: jest.fn().mockReturnThis(), + findByIdAndUpdate: jest.fn().mockReturnThis(), + create: jest.fn().mockResolvedValue(mockUser), + exec: jest.fn().mockResolvedValue(null), + } + mockJwtService = { + sign: jest.fn().mockReturnValue('mock-jwt-token'), + } + mockEmailService = { + sendVerificationCode: jest.fn().mockResolvedValue(true), + } + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + UserService, + { provide: getModelToken('User'), useValue: mockUserModel }, + { provide: JwtService, useValue: mockJwtService }, + { provide: EmailService, useValue: mockEmailService }, + ], + }).compile() + + service = module.get(UserService) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe('sendCode', () => { + it('should return success message', async () => { + const result = await service.sendCode('13800138000') + expect(result).toEqual({ message: '验证码已发送' }) + }) + }) + + describe('loginByPhone', () => { + it('should throw on wrong code', async () => { + await expect(service.loginByPhone('13800138000', 'wrong')) + .rejects.toThrow(HttpException) + }) + + it('should create user on first login and return token', async () => { + mockUserModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(null) }) + await service.sendCode('13800138000') + const result = await service.loginByPhone('13800138000', '123456') + expect(result).toHaveProperty('token', 'mock-jwt-token') + expect(result.user).toHaveProperty('id') + expect(mockUserModel.create).toHaveBeenCalled() + }) + + it('should login existing user', async () => { + mockUserModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(mockUser) }) + await service.sendCode('13800138000') + const result = await service.loginByPhone('13800138000', '123456') + expect(result).toHaveProperty('token') + }) + }) + + describe('sendEmailCode', () => { + it('should reject invalid email', async () => { + await expect(service.sendEmailCode('invalid')) + .rejects.toThrow(HttpException) + }) + + it('should send email verification code', async () => { + const result = await service.sendEmailCode('test@example.com') + expect(result).toEqual({ message: '验证码已发送到邮箱' }) + }) + }) + + describe('loginByEmail', () => { + it('should throw on wrong code', async () => { + await expect(service.loginByEmail('test@example.com', 'wrong')) + .rejects.toThrow(HttpException) + }) + + it('should login with valid email code', async () => { + mockUserModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(mockUser) }) + + const spy = jest.spyOn(mockEmailService, 'sendVerificationCode') + await service.sendEmailCode('test@example.com') + const storedCode = spy.mock.calls[0][1] as string + + const result = await service.loginByEmail('test@example.com', storedCode) + expect(result).toHaveProperty('token') + expect(result.isNew).toBe(false) + }) + }) + + describe('loginByPassword', () => { + it('should throw for nonexistent user', async () => { + mockUserModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(null) }) + await expect(service.loginByPassword('test@example.com', 'pass')) + .rejects.toThrow(HttpException) + }) + }) + + describe('getInfo', () => { + it('should return user info', async () => { + mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(mockUser) }) + const result = await service.getInfo('507f1f77bcf86cd799439011') + expect(result).toHaveProperty('id', mockUser._id) + expect(result).toHaveProperty('phone', mockUser.phone) + }) + + it('should throw for nonexistent user', async () => { + mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(null) }) + await expect(service.getInfo('nonexistent')).rejects.toThrow(HttpException) + }) + }) + + describe('deductRemaining', () => { + it('should decrement remaining count', async () => { + const user = { ...mockUser, remaining: 3, interviewCount: 0, save: jest.fn().mockResolvedValue(true) } + mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(user) }) + await service.deductRemaining('507f1f77bcf86cd799439011') + expect(user.remaining).toBe(2) + expect(user.interviewCount).toBe(1) + expect(user.save).toHaveBeenCalled() + }) + + it('should throw when no remaining', async () => { + const user = { ...mockUser, remaining: 0, save: jest.fn() } + mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(user) }) + await expect(service.deductRemaining('507f1f77bcf86cd799439011')).rejects.toThrow(HttpException) + }) + }) +}) diff --git a/backend/src/modules/user/user.service.ts b/backend/src/modules/user/user.service.ts index b02ebd5..d7dc9b9 100644 --- a/backend/src/modules/user/user.service.ts +++ b/backend/src/modules/user/user.service.ts @@ -1,5 +1,5 @@ import * as bcrypt from 'bcrypt' -import { Injectable, HttpException, HttpStatus } from '@nestjs/common' +import { Injectable, HttpException, HttpStatus, Logger } from '@nestjs/common' import { InjectModel } from '@nestjs/mongoose' import { Model } from 'mongoose' import { JwtService } from '@nestjs/jwt' @@ -12,6 +12,8 @@ const emailCodeStore = new Map() @Injectable() export class UserService { + private readonly logger = new Logger(UserService.name) + constructor( @InjectModel(User.name) private userModel: Model, private jwtService: JwtService, @@ -26,7 +28,7 @@ export class UserService { codeStore.set(phone, { code, expiresAt: Date.now() + 5 * 60 * 1000 }) if (process.env.NODE_ENV !== 'production') { - console.log(`[DEV] Verification code for ${phone}: ${code}`) + this.logger.log(`Verification code for ${phone}: ${code}`) } return { message: '验证码已发送' } } @@ -87,9 +89,11 @@ export class UserService { if (sent) { return { message: '验证码已发送到邮箱' } } - // 邮件发送失败时返回 devCode 方便调试 - console.log(`[EMAIL] Dev code for ${email}: ${code}`) - return { message: '验证码已发送(邮件服务暂不可用,开发模式)', devCode: code } + if (process.env.NODE_ENV !== 'production') { + this.logger.log(`Email code for ${email}: ${code}`) + return { message: '验证码已发送(邮件服务暂不可用,开发模式)', devCode: code } + } + return { message: '验证码已发送,请查收邮件' } } async loginByEmail(email: string, code: string) { diff --git a/backend/test/api.browser.spec.ts b/backend/test/api.browser.spec.ts new file mode 100644 index 0000000..f0b7374 --- /dev/null +++ b/backend/test/api.browser.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from '@playwright/test' + +const BASE = 'http://localhost:3006/api' + +test.describe('Backend API (Playwright)', () => { + test('GET /api/user/info returns 401 without token', async ({ request }) => { + const res = await request.get(`${BASE}/user/info`) + expect(res.status()).toBe(401) + }) + + test('POST /api/user/send-code returns 200', async ({ request }) => { + const res = await request.post(`${BASE}/user/send-code`, { + data: { phone: '13800138000' }, + }) + expect(res.status()).toBe(201) + const body = await res.json() + expect(body.message).toBe('验证码已发送') + }) + + test('POST /api/payment/create returns 401 without auth', async ({ request }) => { + const res = await request.post(`${BASE}/payment/create`, { + data: { plan: 'growth' }, + }) + expect(res.status()).toBe(401) + }) + + test('POST /api/progress/checkin returns 401 without auth', async ({ request }) => { + const res = await request.post(`${BASE}/progress/checkin`) + expect(res.status()).toBe(401) + }) + + test('POST /api/contribution returns 401 without auth', async ({ request }) => { + const res = await request.post(`${BASE}/contribution`, { + data: { company: 'Test', position: '前端工程师' }, + }) + expect(res.status()).toBe(401) + }) + + test('GET /api/progress requires auth', async ({ request }) => { + const res = await request.get(`${BASE}/progress`) + expect(res.status()).toBe(401) + }) + + test('GET /api/admin/check requires auth', async ({ request }) => { + const res = await request.get(`${BASE}/admin/check`) + expect(res.status()).toBe(401) + }) +}) diff --git a/backend/test/app.e2e-spec.ts b/backend/test/app.e2e-spec.ts new file mode 100644 index 0000000..c77e0c8 --- /dev/null +++ b/backend/test/app.e2e-spec.ts @@ -0,0 +1,187 @@ +import request from 'supertest' +import { Test, TestingModule } from '@nestjs/testing' +import { INestApplication, ValidationPipe } from '@nestjs/common' +import { getModelToken } from '@nestjs/mongoose' +import { JwtService } from '@nestjs/jwt' +import { WechatPayService } from '../src/modules/payment/wechat-pay.service' +import { AppModule } from '../src/app.module' + +function makeChain(execValue: any = null) { + const chain: Record = {} + const methods = ['find', 'findOne', 'findById', 'findByIdAndUpdate', 'sort', 'skip', 'limit', 'select', 'lean', 'countDocuments'] + for (const m of methods) { + chain[m] = jest.fn(() => chain) + } + chain.exec = jest.fn().mockResolvedValue(execValue) + chain.save = jest.fn().mockResolvedValue(true) + return chain +} + +function mockModel(extra: Record = {}, defaultExecValue: any = null) { + const chain = makeChain(defaultExecValue) + return { + find: jest.fn(() => chain), + findOne: jest.fn(() => chain), + findById: jest.fn(() => chain), + findByIdAndUpdate: jest.fn(() => chain), + create: jest.fn(), + countDocuments: jest.fn(() => chain), + ...extra, + } +} + +describe('API Integration (e2e)', () => { + let app: INestApplication + let authToken: string + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }) + .overrideProvider(WechatPayService).useValue({ + nativePay: jest.fn().mockResolvedValue({ codeUrl: 'weixin://pay/test' }), + jsapiPay: jest.fn().mockResolvedValue({ paySign: 'test', nonceStr: 'test', package: 'test', timeStamp: '0', signType: 'RSA' }), + queryOrder: jest.fn().mockResolvedValue({ trade_state: 'SUCCESS' }), + verifyAndDecrypt: jest.fn().mockReturnValue({ out_trade_no: 'TEST', transaction_id: 'wx_test' }), + }) + .overrideProvider(getModelToken('User')).useValue(mockModel({ countDocuments: jest.fn().mockResolvedValue(0) })) + .overrideProvider(getModelToken('Interview')).useValue(mockModel({}, [])) + .overrideProvider(getModelToken('PaymentOrder')).useValue(mockModel({}, null)) + .overrideProvider(getModelToken('Progress')).useValue(mockModel({}, null)) + .overrideProvider(getModelToken('CompanyBank')).useValue(mockModel()) + .overrideProvider(getModelToken('Contribution')).useValue(mockModel()) + .overrideProvider(getModelToken('SiteConfig')).useValue(mockModel()) + .overrideProvider(getModelToken('Resume')).useValue(mockModel()) + .compile() + + app = moduleFixture.createNestApplication() + app.setGlobalPrefix('api') + app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true })) + await app.init() + + const jwtService = app.get(JwtService) + authToken = jwtService.sign({ userId: '507f1f77bcf86cd799439011', phone: '13800138000' }) + }) + + afterAll(async () => { + await app.close() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe('Unauthenticated access', () => { + it('GET /api/user/info should return 401', async () => { + const res = await request(app.getHttpServer()).get('/api/user/info') + expect(res.status).toBe(401) + }) + + it('POST /api/user/send-code should be public', async () => { + const res = await request(app.getHttpServer()) + .post('/api/user/send-code') + .send({ phone: '13800138000' }) + expect(res.status).toBe(201) + }) + + it('POST /api/payment/create should return 401', async () => { + const res = await request(app.getHttpServer()) + .post('/api/payment/create') + .send({ plan: 'growth' }) + expect(res.status).toBe(401) + }) + + it('GET /api/progress should return 401', async () => { + const res = await request(app.getHttpServer()).get('/api/progress') + expect(res.status).toBe(401) + }) + + it('POST /api/progress/checkin should return 401', async () => { + const res = await request(app.getHttpServer()).post('/api/progress/checkin') + expect(res.status).toBe(401) + }) + }) + + describe('Authenticated access', () => { + const headers = () => ({ Authorization: `Bearer ${authToken}` }) + + it('POST /api/payment/create should validate plan', async () => { + const res = await request(app.getHttpServer()) + .post('/api/payment/create') + .set(headers()) + .send({ plan: 'invalid' }) + expect(res.status).toBe(400) + }) + + it('POST /api/payment/jsapi should handle missing wxOpenid', async () => { + const userModel = app.get(getModelToken('User')) + const chain = makeChain({ plan: 'free', wxOpenid: null }) + userModel.findById.mockReturnValueOnce(chain) + + const res = await request(app.getHttpServer()) + .post('/api/payment/jsapi') + .set(headers()) + .send({ plan: 'growth' }) + expect(res.status).toBe(400) + }) + + it('GET /api/admin/check should check admin status', async () => { + const userModel = app.get(getModelToken('User')) + const chain = makeChain({ role: 'user' }) + userModel.findById.mockReturnValueOnce(chain) + + const res = await request(app.getHttpServer()) + .get('/api/admin/check') + .set(headers()) + expect(res.status).toBe(200) + expect(res.body.isAdmin).toBe(false) + }) + + it('POST /api/progress/redeem should validate item', async () => { + const res = await request(app.getHttpServer()) + .post('/api/progress/redeem') + .set(headers()) + .send({ item: 'nonexistent' }) + expect(res.status).toBe(400) + }) + + it('GET /api/progress should return default progress for new user', async () => { + const progressModel = app.get(getModelToken('Progress')) + const nullChain = makeChain(null) + progressModel.findOne.mockReturnValueOnce(nullChain) + progressModel.create.mockResolvedValue({ + userId: '507f1f77bcf86cd799439011', + totalInterviews: 0, + completedInterviews: 0, + streak: 0, + points: 0, + totalCheckins: 0, + recentScores: [], + streakHistory: [], + lastCheckinDate: null, + }) + + const res = await request(app.getHttpServer()) + .get('/api/progress') + .set(headers()) + expect(res.status).toBe(200) + expect(res.body).toHaveProperty('totalInterviews', 0) + expect(res.body).toHaveProperty('points') + }) + }) + + describe('Payment flow', () => { + const headers = () => ({ Authorization: `Bearer ${authToken}` }) + + it('POST /api/payment/check should verify order ownership', async () => { + const orderModel = app.get(getModelToken('PaymentOrder')) + const chain = makeChain(null) + orderModel.findOne.mockReturnValueOnce(chain) + + const res = await request(app.getHttpServer()) + .get('/api/payment/check/ORD123') + .set(headers()) + expect(res.status).toBe(404) + }) + }) +}) diff --git a/backend/test/jest-setup.ts b/backend/test/jest-setup.ts new file mode 100644 index 0000000..68d11fe --- /dev/null +++ b/backend/test/jest-setup.ts @@ -0,0 +1 @@ +process.env.JWT_SECRET = 'test-jwt-secret-for-e2e-tests' diff --git a/docs/PROJECT-STATUS.md b/docs/PROJECT-STATUS.md index f7a697a..b9215c8 100644 --- a/docs/PROJECT-STATUS.md +++ b/docs/PROJECT-STATUS.md @@ -1,8 +1,8 @@ -# 职引项目 · 状态报告 v4.1 +# 职引项目 · 状态报告 v4.3 -> **项目版本**: v4.2 -> **更新时间**: 2026-06-09 -> **项目状态**: 🚀 Phase 0.5 壁垒构建完成 + 全量代码评审修复 +> **项目版本**: v4.3 +> **更新时间**: 2026-06-11 +> **项目状态**: ✅ 代码质量修复 + 全量测试体系搭建完成 --- @@ -23,17 +23,16 @@ | 模块 | 完成度 | 说明 | |------|------|------| -| 后端 API | **98%** | 核心 + Phase 0.5 接口全部实现并编译通过 | -| 前端页面 | **85%** | 16 个页面全部含真实 API 调用,有真实实现 | +| 后端 API | **98%** | 核心 + 护城河 P0-P5 全部实现 | +| 前端页面 | **85%** | 16 个页面含真实 API 调用 | | AI 面试模拟 | **95%** | 多轮对话 + 评分 + 报告 + 进度追踪 | -| 简历诊断/优化 | **95%** | 完整代码,文件上传 + AI 分析 + 下载 | -| 支付系统(微信) | **95%** | API v3 完整对接,含签名/解密/回调/生产密钥 | -| 会员系统 | **100%** | 成长版(¥19.9) + 冲刺版(¥49.9)完整实现,含权益扣减 | -| 进步轨迹雷达图 | **100%** | 后端维度统计 + 前端雷达图/打卡日历 | -| 面经贡献系统 | **100%** | 贡献提交 + 公司题库自动去重/频次统计 | -| 每日一题 | **90%** | 读取 + 定时推送(早8点) + 微信订阅消息,缺模板ID配置 | -| 微信登录 | **70%** | 后端接口齐,前端待联调真实 appid | -| 生产部署 | **50%** | 服务器已购买,域名已配置,微信支付证书已就位,miniprogram-ci 编译上传脚本就绪 | +| 简历诊断/优化 | **95%** | 文件上传 + AI 分析 + 下载 | +| 支付系统(微信) | **95%** | API v3 完整对接,含真实证书 | +| 会员系统 | **100%** | 成长版 + 冲刺版,含权益扣减 | +| 护城河 P0-P5 | **100%** | AI 结构化 / 行业基准 / VIP 过期 / 分享卡片 / 打卡积分 / 岗位匹配 | +| 测试体系 | **85%** | 43 单元 + 11 e2e + 7 前端 + Playwright 框架 | +| 代码质量 | **95%** | console→Logger,as any 类型化,空 catch 检查 | +| 安全审计 | **90%** | JWT 硬编码 / 凭据泄漏 / IDOR / NoSQL 注入 全部修复 | | 小程序审核 | **0%** | 类目已备案,未提交审核 | --- @@ -51,118 +50,149 @@ | 使用次数限制 | ✅ | N/A | **完成** | | 连续打卡(进步轨迹) | ✅ | ✅ | **完成** | -### 3.2 数据飞轮 (Phase 0.5) -| 功能 | 后端 | 前端 | 状态 | -|------|------|------|------| -| 面经贡献 | ✅ | ✅ | **完成** | -| 公司-岗位-题库映射 | ✅ | N/A | **完成** | -| 脱敏存储 | ✅ | N/A | **完成** | -| 题库自动扩充(去重+频次) | ✅ | N/A | **完成** | +### 3.2 护城河 P0-P5 +| 优先级 | 功能 | 后端 | 文件 | 状态 | +|--------|------|------|------|------| +| **P0** | 公司真题结构化(AI 自动处理) | ✅ | `contribution.controller.ts`, `contribution.schema.ts` | **完成** | +| **P1** | 行业基准线 + 用户百分位 | ✅ | `benchmark.service.ts`, `progress.controller.ts` | **完成** | +| **P2** | VIP 过期 cron 自动降级 | ✅ | `vip-expiry.service.ts` | **完成** | +| **P3** | 分享卡片(canvas 生成) | ✅ | `report.vue` | **完成** | +| **P4** | 每日打卡 + 积分兑换 | ✅ | `progress.controller.ts`, `progress.schema.ts` | **完成** | +| **P5** | 岗位匹配度预测 | ✅ | `progress.controller.ts` | **完成** | -### 3.3 留存入围 (Phase 0.5) -| 功能 | 后端 | 前端 | 状态 | -|------|------|------|------| -| 进步轨迹雷达图 | ✅ | ✅ | **完成** | -| 历史对比分析 | ✅ | ✅ | **完成** | -| 日历打卡视图 | N/A | ✅ | **完成** | -| 每日一题推送 | ✅ 定时推送(早8点) | ✅ 首页展示 | **完成**(缺微信模板ID) | - -### 3.4 用户系统 +### 3.3 用户系统 | 功能 | 后端 | 前端 | 状态 | |------|------|------|------| | 手机验证码登录 | ✅ | ✅ | **完成** | | 邮箱验证码登录 | ✅ | ✅ | **完成** | | 密码登录/注册 | ✅ | ✅ | **完成** | -| 微信静默登录 | ✅ 有接口 | ✅ 有调用 | ⚠️ 缺真实 appid | +| 微信静默登录 | ✅ | ✅ | ⚠️ 缺真实 appid 联调 | | JWT 认证 | ✅ | ✅ | **完成** | | 个人信息设置 | ✅ | ✅ | **完成** | -### 3.5 商业化 +### 3.4 商业化 | 功能 | 后端 | 前端 | 状态 | |------|------|------|------| | 免费版额度(日2次/5轮) | ✅ | ✅ | **完成** | | 成长版 ¥19.9/月 | ✅ | ✅ | **完成** | | 冲刺版 ¥49.9/月(含权益扣减) | ✅ | ✅ | **完成** | -| 每日一题定时推送(微信订阅消息) | ✅ | N/A | **完成**(需配置模板ID) | -| 微信支付 Native QR | ✅ | ✅ H5 | **完成** | -| 微信支付 JSAPI | ✅ | ✅ MP | **完成** | +| 微信支付 Native QR / JSAPI | ✅ | ✅ H5+MP | **完成** | | 支付回调/自动开会员 | ✅ | N/A | **完成** | +| 每日一题定时推送 | ✅ | N/A | **完成**(需配置模板ID) | | 会员状态/套餐查询 | ✅ | ✅ | **完成** | -### 3.6 简历 +### 3.5 简历 | 功能 | 后端 | 前端 | 状态 | |------|------|------|------| -| AI 简历诊断 | ✅ | ✅ | **完成** | -| AI 简历优化 | ✅ | ✅ | **完成** | +| AI 简历诊断/优化 | ✅ | ✅ | **完成** | | 简历 CRUD | ✅ | ✅ | **完成** | | 文件上传(PDF/图片) | ✅ | ✅ | **完成** | | 结果下载(TXT/HTML) | N/A | ✅ | **完成** | --- -## 四、后端模块清单 +## 四、测试体系 + +| 层次 | 工具 | 文件 | 测试数 | 状态 | +|------|------|------|--------|------| +| 后端单元 | Jest | `benchmark.service.spec.ts` | 15 | ✅ | +| | | `user.service.spec.ts` | 13 | ✅ | +| | | `payment.controller.spec.ts` | 15 | ✅ | +| 后端集成 | Supertest+Jest | `app.e2e-spec.ts` | 11 | ✅ | +| 前端单元 | Vitest | `config.spec.ts` | 7 | ✅ | +| 浏览器自动化 | Playwright | `api.browser.spec.ts` | 7 | ✅ (需后端运行) | +| **总计** | | | **61** | ✅ | + +| 命令 | 用途 | +|------|------| +| `npm test` | 43 个单元测试 | +| `npm run test:e2e` | 11 个集成测试 | +| `npm run test:cov` | 覆盖率报告 | +| `npm run test:browser` | Playwright API 测试 | +| `cd zhiyin-app && npm test` | 7 个前端测试 | + +--- + +## 五、安全修复清单 + +| 严重度 | 问题 | 文件 | 修复 | +|--------|------|------|------| +| 🔴 CRITICAL | JWT 硬编码 fallback (3处) | `jwt.strategy.ts`, `app.module.ts`, `user.module.ts` | 移除 `zhiyin-jwt-secret` 默认值 | +| 🟠 HIGH | seed_admin.js 写死 MongoDB 凭据 | `.gitignore` | 加入 ignore 列表 | +| 🟡 MEDIUM | 邮箱验证码泄漏 | `user.service.ts:91` | 仅开发环境暴露 devCode | +| 🟡 MEDIUM | 支付订单查询 IDOR | `payment.controller.ts:123` | 校验 userId 归属 | +| 🟡 MEDIUM | 管理后台 NoSQL 注入 | `admin.controller.ts:61` | 正则特殊字符转义 | +| 🟢 LOW | console.log 泄漏敏感信息 (2处) | `user.service.ts:29,91` | 替换为 Logger | + +--- + +## 六、代码质量修复 + +| 类别 | 文件 | 修复 | +|------|------|------| +| `console.log`→Logger | `user.service.ts` | 开发版日志改为 `this.logger.log()` | +| `as any`→类型化 (11处) | 7 个文件 | Mongoose `_id`/`createdAt` 直接访问;基准类型用 `PositionBenchmark` | +| Schema 联合类型 | `progress.schema.ts:63` | `lastCheckinDate?: Date \| null` 加 `@Prop({ type: Date })` | +| Module 依赖缺失 | `progress.module.ts` | 缺少 `UserModel` 注入导致启动崩溃 | + +--- + +## 七、后端模块清单 | 模块 | 文件 | 状态 | 说明 | |------|------|------|------| -| `user` | controller + service + schema | ✅ | 手机/邮箱/密码/微信多种登录方式 | -| `interview` | controller + service + schema | ✅ | AI 面试核心,含进度追踪调用 | -| `ai` | module + service | ✅ | AI 模型调用封装(主/备切换) | -| `analyze` | controller + module + service | ✅ | 简历诊断/优化 | +| `user` | controller + service + schema | ✅ | 手机/邮箱/密码/微信登录 | +| `interview` | controller + service + schema | ✅ | AI 面试核心 | +| `ai` | module + service | ✅ | AI 主/备模型切换,TLS 修复 | +| `analyze` | controller + module + service | ✅ | 诊断/优化/技能缺口 | | `resume` | controller + service + schema | ✅ | 简历 CRUD | -| `member` | controller | ✅ | 会员套餐/状态/冲刺版权益扣减 | -| `payment` | controller + service + schema | ✅ | 微信支付 v3 完整对接(生产密钥已配) | -| `positions` | controller + schema | ✅ | 热门岗位 CRUD | -| `upload` | controller + module | ✅ | 文件上传 | +| `member` | controller | ✅ | 会员套餐/权益扣减 | +| `payment` | controller + service + schema | ✅ | 微信支付 v3,含证书 | +| `progress` | controller + schema + benchmark service | ✅ | 打卡/积分/基准/匹配 | +| `contribution` | controller + schema (×2) | ✅ | 面经 + AI 结构化 + 公司题库 | +| `schedule` | module + service (×3) | ✅ | VIP 过期 / 每日一题 / 微信 token | | `admin` | controller + module | ✅ | 管理后台 | | `email` | module + service | ✅ | 邮件发送 | -| `progress` | controller + schema | ✅ | 进步轨迹四维统计 | -| `contribution` | controller + schema (×2) | ✅ | 面经贡献 + 公司题库 | -| `daily-question` | controller + schema | ✅ | 读取 + 定时推送 @schedule | -| `schedule` | module + service (×2) | ✅ | 每日一题早8点推送 + 微信token管理 | +| `upload` | controller + module | ✅ | 文件上传 | --- -## 五、前端页面清单 +## 八、前端页面清单 -| 页面 | 路径 | 类型 | 状态 | -|------|------|------|------| -| 首页 | index/index | Tab | ✅ 岗位/每日一题/功能入口 | -| 登录 | login/login | 页面 | ✅ 5 种登录方式 + 注册 | -| 面试模拟 | interview/interview | 页面 | ✅ 多轮对话 + 计时 | -| 面试报告 | report/report | 页面 | ✅ 评分/分析/全文回放 | -| 历史记录 | history/history | Tab | ✅ 筛选/统计/跳转报告 | -| 个人中心 | user/user | Tab | ✅ 用户信息/统计/管理员入口 | -| 会员中心 | member/member | 页面 | ✅ 套餐对比 + 支付流程 | -| 进步轨迹 | progress/progress | 页面 | ✅ 雷达图 + 打卡日历 | -| 面经贡献 | contribute/contribute | 页面 | ✅ 表单提交 | -| 简历优化 | resume/resume | 页面 | ✅ 诊断/优化/上传/下载 | -| 优化结果 | result/result | 页面 | ✅ 双模式结果展示 | -| 实习搜索 | internship/internship | 页面 | ✅ 热门岗位列表 | -| 管理后台 | admin/admin | 页面 | ✅ 仪表盘 | -| 关于 | about/about | 页面 | ✅ | -| 用户协议 | agreement/agreement | 页面 | ✅ | -| 隐私政策 | privacy/privacy | 页面 | ✅ | +| 页面 | 路径 | 状态 | +|------|------|------| +| 首页 | index/index | ✅ 岗位/每日一题/功能入口 | +| 登录 | login/login | ✅ 5 种登录方式 + 注册 | +| 面试模拟 | interview/interview | ✅ 多轮对话 + 计时 | +| 面试报告 | report/report | ✅ 评分/分析/全文回放/分享卡片 | +| 历史记录 | history/history | ✅ 筛选/统计 | +| 个人中心 | user/user | ✅ 信息/统计/管理员入口 | +| 会员中心 | member/member | ✅ 套餐对比 + 支付 | +| 进步轨迹 | progress/progress | ✅ 雷达图 + 打卡日历 | +| 面经贡献 | contribute/contribute | ✅ 表单提交 | +| 简历优化 | resume/resume | ✅ 诊断/优化/上传/下载 | +| 实习搜索 | internship/internship | ✅ 热门岗位 | +| 管理后台 | admin/admin | ✅ 仪表盘 | +| 关于/协议/隐私 | about/agreement/privacy | ✅ | --- -## 六、技术债务 +## 九、技术债务 | 问题 | 影响 | 优先级 | -|------|------|------| -| 微信登录未用真实 appid 联调 | 无法真机测试微信登录 | P0 | -| 前端两套 API 调用方式(`uni.request` vs `apiService`) | 代码维护负担 | P2 | -| 前端无状态管理(Pinia) + 无组件复用 | 代码重复 | P2 | +|------|------|--------| +| 微信登录需真实 appid 联调 | 无法真机测试 | P0 | +| 前端两套 API 调用方式 | 维护负担 | P2 | | AI 调用无重试机制 | 偶发失败 | P1 | -| 无单元测试 | 回归风险 | P2 | +| UploadController 含 pdf-parse 原生依赖 | e2e 测试 open handle | P3 | --- -## 七、变更记录 +## 十、变更记录 -| 日期 | 变更内容 | 操作者 | -|------|----------|--------| -| 2026-06-02 | 项目状态初版,测试 10/10 通过 | AI | -| 2026-06-05 | 战略升级:文档重构 + 新增功能启动 | 小之 | -| 2026-06-09 | 全面更新:Phase 0.5 功能实际已完成,修正完成度数据与模块清单 | AI | -| 2026-06-09 | 更新部署状态:服务器已购,域名 zhiyinwx.yzrcloud.cn / zhiyin.yzrcloud.cn 已配 | 小之 | -| 2026-06-09 | v4.2 冲刺版+每日推送+支付修复+全量代码评审 | AI | +| 日期 | 版本 | 变更内容 | 操作者 | +|------|------|----------|--------| +| 2026-06-02 | v1.0 | 项目状态初版 | AI | +| 2026-06-05 | v2.0 | 战略升级:文档重构 + 新增功能启动 | 小之 | +| 2026-06-09 | v4.2 | 冲刺版+每日推送+支付修复+全量代码评审 | AI | +| 2026-06-11 | **v4.3** | **安全修复 5 项 + 代码质量 14 处 + 测试体系 61 项 + 护城河 P0-P5 全部验证** | AI | diff --git a/zhiyin-app/package-lock.json b/zhiyin-app/package-lock.json index fb9ecf2..e009bba 100644 --- a/zhiyin-app/package-lock.json +++ b/zhiyin-app/package-lock.json @@ -23,10 +23,13 @@ "@dcloudio/uni-automator": "3.0.0-4060620250520001", "@dcloudio/uni-cli-shared": "3.0.0-4060620250520001", "@dcloudio/vite-plugin-uni": "3.0.0-4060620250520001", + "@vue/test-utils": "^2.4.11", + "jsdom": "^29.1.1", "miniprogram-ci": "^2.1.31", "sass": "^1.70.0", "typescript": "^5.3.0", - "vite": "^5.2.0" + "vite": "^5.2.0", + "vitest": "^4.1.8" } }, "node_modules/@ampproject/remapping": { @@ -40,6 +43,78 @@ "node": ">=6.0.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmmirror.com/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "license": "MIT", @@ -2396,6 +2471,40 @@ "license": "MIT", "peer": true }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@bramus/specificity/node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/@bramus/specificity/node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/@cronvel/get-pixels": { "version": "3.4.1", "resolved": "https://registry.npmmirror.com/@cronvel/get-pixels/-/get-pixels-3.4.1.tgz", @@ -2428,6 +2537,121 @@ "node": ">=12.13.0" } }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/@csstools/css-color-parser/-/css-color-parser-4.1.3.tgz", + "integrity": "sha512-DOgvIPkikIOixQRlD4YF31VN6fLLUTdrzhfRbis8vm0kMTgIbEPX0Ip/YX9fOeV9iywAS4sUUbTclpan7yYP8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@dcloudio/types": { "version": "3.4.31", "license": "Apache-2.0" @@ -2967,6 +3191,40 @@ "vite": "^5.2.8" } }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.20.2", "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", @@ -3238,6 +3496,24 @@ "node": ">=18" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { "version": "0.20.2", "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", @@ -3254,6 +3530,24 @@ "node": ">=12" } }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/openbsd-x64": { "version": "0.20.2", "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", @@ -3270,6 +3564,24 @@ "node": ">=12" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.20.2", "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", @@ -3502,6 +3814,24 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmmirror.com/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -3618,6 +3948,109 @@ "node": ">= 10" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "dev": true, @@ -4364,6 +4797,25 @@ "dev": true, "license": "MIT" }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmmirror.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -4403,6 +4855,23 @@ "node": ">= 8" } }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmmirror.com/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@parcel/watcher": { "version": "2.5.6", "dev": true, @@ -4457,6 +4926,17 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmmirror.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -4530,6 +5010,270 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/pluginutils": { "version": "5.3.0", "license": "MIT", @@ -4922,6 +5666,13 @@ "@sinonjs/commons": "^1.7.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@swc/core": { "version": "1.4.14", "resolved": "https://registry.npmmirror.com/@swc/core/-/core-1.4.14.tgz", @@ -5157,6 +5908,8 @@ }, "node_modules/@tootallnate/once": { "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "dev": true, "license": "MIT", "peer": true, @@ -5164,6 +5917,17 @@ "node": ">= 6" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "dev": true, @@ -5205,6 +5969,24 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmmirror.com/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "license": "MIT" @@ -5346,6 +6128,92 @@ "vue": "^3.0.0" } }, + "node_modules/@vitest/expect": { + "version": "4.1.8", + "resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.8", + "resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.8", + "resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.8", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.8", + "resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.8", + "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.8", + "resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@vue/babel-helper-vue-transform-on": { "version": "1.5.0", "dev": true, @@ -5599,6 +6467,27 @@ "version": "3.4.21", "license": "MIT" }, + "node_modules/@vue/test-utils": { + "version": "2.4.11", + "resolved": "https://registry.npmmirror.com/@vue/test-utils/-/test-utils-2.4.11.tgz", + "integrity": "sha512-GDqaqZsA6m2E5vNzej0aYiIb6BX8xV9pNSbbbXKOfEYwg7ZNblVX8suyqmUBThq8VIrgAJNxn+z72hVtUeiWHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-beautify": "^1.14.9", + "vue-component-type-helpers": "^3.0.0" + }, + "peerDependencies": { + "@vue/compiler-dom": "3.x", + "@vue/server-renderer": "3.x", + "vue": "3.x" + }, + "peerDependenciesMeta": { + "@vue/server-renderer": { + "optional": true + } + } + }, "node_modules/@xmldom/xmldom": { "version": "0.9.10", "resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.9.10.tgz", @@ -5611,10 +6500,23 @@ }, "node_modules/abab": { "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", "dev": true, "license": "BSD-3-Clause", "peer": true }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/abort-controller/-/abort-controller-3.0.0.tgz", @@ -5652,6 +6554,8 @@ }, "node_modules/acorn-globals": { "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/acorn-globals/-/acorn-globals-6.0.0.tgz", + "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", "dev": true, "license": "MIT", "peer": true, @@ -5662,6 +6566,8 @@ }, "node_modules/acorn-globals/node_modules/acorn": { "version": "7.4.1", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true, "license": "MIT", "peer": true, @@ -5685,6 +6591,8 @@ }, "node_modules/acorn-walk": { "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", "dev": true, "license": "MIT", "peer": true, @@ -5709,6 +6617,8 @@ }, "node_modules/agent-base": { "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "dev": true, "license": "MIT", "peer": true, @@ -5997,6 +6907,16 @@ "node": ">=0.8" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmmirror.com/async/-/async-3.2.6.tgz", @@ -7429,6 +8349,16 @@ "node": ">=0.10.0" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/big-integer": { "version": "1.6.52", "resolved": "https://registry.npmmirror.com/big-integer/-/big-integer-1.6.52.tgz", @@ -7577,6 +8507,8 @@ }, "node_modules/browser-process-hrtime": { "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", "dev": true, "license": "BSD-2-Clause", "peer": true @@ -7895,6 +8827,16 @@ "follow-redirects": "^1.15.6" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "dev": true, @@ -8563,12 +9505,16 @@ }, "node_modules/cssom": { "version": "0.4.4", + "resolved": "https://registry.npmmirror.com/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", "dev": true, "license": "MIT", "peer": true }, "node_modules/cssstyle": { "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", "dev": true, "license": "MIT", "peer": true, @@ -8581,6 +9527,8 @@ }, "node_modules/cssstyle/node_modules/cssom": { "version": "0.3.8", + "resolved": "https://registry.npmmirror.com/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "dev": true, "license": "MIT", "peer": true @@ -8613,17 +9561,17 @@ } }, "node_modules/data-urls": { - "version": "2.0.0", + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "abab": "^2.0.3", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0" + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" }, "engines": { - "node": ">=10" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/dateformat": { @@ -8701,8 +9649,7 @@ "node_modules/decimal.js": { "version": "10.6.0", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/decode-uri-component": { "version": "0.2.2", @@ -9097,7 +10044,6 @@ "version": "2.1.2", "dev": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -9188,6 +10134,9 @@ }, "node_modules/domexception": { "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/domexception/-/domexception-2.0.1.tgz", + "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "deprecated": "Use your platform's native DOMException instead", "dev": true, "license": "MIT", "peer": true, @@ -9200,6 +10149,8 @@ }, "node_modules/domexception/node_modules/webidl-conversions": { "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", "dev": true, "license": "BSD-2-Clause", "peer": true, @@ -9391,6 +10342,13 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmmirror.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -9402,6 +10360,74 @@ "safer-buffer": "^2.1.0" } }, + "node_modules/editorconfig": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/editorconfig/-/editorconfig-1.0.7.tgz", + "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "^9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/editorconfig/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ee-first": { "version": "1.1.1", "dev": true, @@ -9520,6 +10546,8 @@ }, "node_modules/es-set-tostringtag": { "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "license": "MIT", "peer": true, @@ -9608,6 +10636,8 @@ }, "node_modules/escodegen": { "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", "dev": true, "license": "BSD-2-Clause", "peer": true, @@ -10113,6 +11143,16 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "4.22.2", "dev": true, @@ -10314,6 +11354,24 @@ "pend": "~1.2.0" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -10470,6 +11528,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmmirror.com/forever-agent/-/forever-agent-0.6.1.tgz", @@ -10482,6 +11570,8 @@ }, "node_modules/form-data": { "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-3.0.4.tgz", + "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==", "dev": true, "license": "MIT", "peer": true, @@ -11293,15 +12383,16 @@ "license": "ISC" }, "node_modules/html-encoding-sniffer": { - "version": "2.0.1", + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "whatwg-encoding": "^1.0.5" + "@exodus/bytes": "^1.6.0" }, "engines": { - "node": ">=10" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/html-escaper": { @@ -11373,6 +12464,8 @@ }, "node_modules/http-proxy-agent": { "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", "dev": true, "license": "MIT", "peer": true, @@ -11403,6 +12496,8 @@ }, "node_modules/https-proxy-agent": { "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "dev": true, "license": "MIT", "peer": true, @@ -11819,8 +12914,7 @@ "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/is-retry-allowed": { "version": "1.2.0", @@ -11993,6 +13087,22 @@ "node": ">= 4" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jest": { "version": "27.0.4", "dev": true, @@ -12200,6 +13310,228 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/jest-environment-jsdom/node_modules/data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-environment-jsdom/node_modules/html-encoding-sniffer": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "whatwg-encoding": "^1.0.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jsdom": { + "version": "16.7.0", + "resolved": "https://registry.npmmirror.com/jsdom/-/jsdom-16.7.0.tgz", + "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "abab": "^2.0.5", + "acorn": "^8.2.4", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.3.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.1", + "domexception": "^2.0.1", + "escodegen": "^2.0.0", + "form-data": "^3.0.0", + "html-encoding-sniffer": "^2.0.1", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.0", + "parse5": "6.0.1", + "saxes": "^5.0.1", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.0.0", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.5.0", + "ws": "^7.4.6", + "xml-name-validator": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/jest-environment-jsdom/node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-environment-jsdom/node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-environment-jsdom/node_modules/tr46": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/w3c-xmlserializer": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", + "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "xml-name-validator": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-environment-jsdom/node_modules/webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=10.4" + } + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-url": { + "version": "8.7.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-8.7.0.tgz", + "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lodash": "^4.7.0", + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-environment-jsdom/node_modules/ws": { + "version": "7.5.11", + "resolved": "https://registry.npmmirror.com/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true, + "license": "Apache-2.0", + "peer": true + }, "node_modules/jest-environment-node": { "version": "27.5.1", "dev": true, @@ -12657,6 +13989,82 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmmirror.com/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-beautify/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/js-beautify/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-cookie": { + "version": "3.0.8", + "resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.8.tgz", + "integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==", + "dev": true, + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "license": "MIT" @@ -12682,44 +14090,39 @@ "license": "MIT" }, "node_modules/jsdom": { - "version": "16.7.0", + "version": "29.1.1", + "resolved": "https://registry.npmmirror.com/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "abab": "^2.0.5", - "acorn": "^8.2.4", - "acorn-globals": "^6.0.0", - "cssom": "^0.4.4", - "cssstyle": "^2.3.0", - "data-urls": "^2.0.0", - "decimal.js": "^10.2.1", - "domexception": "^2.0.1", - "escodegen": "^2.0.0", - "form-data": "^3.0.0", - "html-encoding-sniffer": "^2.0.1", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "6.0.1", - "saxes": "^5.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^2.0.0", - "webidl-conversions": "^6.1.0", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.5.0", - "ws": "^7.4.6", - "xml-name-validator": "^3.0.0" + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=10" + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" }, "peerDependencies": { - "canvas": "^2.5.0" + "canvas": "^3.0.0" }, "peerDependenciesMeta": { "canvas": { @@ -12727,27 +14130,62 @@ } } }, - "node_modules/jsdom/node_modules/ws": { - "version": "7.5.10", + "node_modules/jsdom/node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", + "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8.3.0" - }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "css-tree": "^3.2.1" }, "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { + "css-tree": { "optional": true } } }, + "node_modules/jsdom/node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsdom/node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/jsesc": { "version": "3.1.0", "license": "MIT", @@ -13120,6 +14558,267 @@ "immediate": "~3.0.5" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lilconfig": { "version": "2.1.0", "license": "MIT", @@ -13751,6 +15450,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/miniprogram-ci": { "version": "2.1.31", "resolved": "https://registry.npmmirror.com/miniprogram-ci/-/miniprogram-ci-2.1.31.tgz", @@ -16743,6 +18452,22 @@ "version": "2.0.44", "license": "MIT" }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmmirror.com/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmmirror.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -16844,7 +18569,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.23", + "version": "2.2.24", + "resolved": "https://registry.npmmirror.com/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", "dev": true, "license": "MIT", "peer": true @@ -16880,6 +18607,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/obug/-/obug-2.1.2.tgz", + "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/omggif": { "version": "1.0.10", "license": "MIT" @@ -17122,6 +18863,13 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/pako": { "version": "1.0.11", "license": "(MIT AND Zlib)" @@ -17205,10 +18953,30 @@ } }, "node_modules/parse5": { - "version": "6.0.1", + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", "dev": true, "license": "MIT", - "peer": true + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } }, "node_modules/parseurl": { "version": "1.3.3", @@ -17246,6 +19014,30 @@ "version": "1.0.7", "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/path-to-regexp": { "version": "0.1.13", "dev": true, @@ -17497,7 +19289,9 @@ } }, "node_modules/postcss": { - "version": "8.5.14", + "version": "8.5.15", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "funding": [ { "type": "opencollective", @@ -17514,7 +19308,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -18357,6 +20151,8 @@ }, "node_modules/querystringify": { "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "dev": true, "license": "MIT", "peer": true @@ -18790,6 +20586,8 @@ }, "node_modules/requires-port": { "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true, "license": "MIT", "peer": true @@ -18892,6 +20690,40 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, "node_modules/rollup": { "version": "4.60.4", "devOptional": true, @@ -19056,15 +20888,16 @@ } }, "node_modules/saxes": { - "version": "5.0.1", + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "xmlchars": "^2.2.0" }, "engines": { - "node": ">=10" + "node": ">=v12.22.7" } }, "node_modules/scule": { @@ -19290,6 +21123,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "3.0.7", "dev": true, @@ -19482,6 +21322,13 @@ "node": ">=10" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "dev": true, @@ -19490,6 +21337,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/stdin-discarder": { "version": "0.1.0", "resolved": "https://registry.npmmirror.com/stdin-discarder/-/stdin-discarder-0.1.0.tgz", @@ -19583,6 +21437,22 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "dev": true, @@ -19594,6 +21464,20 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "4.0.0", "dev": true, @@ -19787,8 +21671,7 @@ "node_modules/symbol-tree": { "version": "3.2.4", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/sync-message": { "version": "0.0.12", @@ -19965,10 +21848,74 @@ "version": "1.7.1", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinycolor2": { "version": "1.6.0", "license": "MIT" }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.2", + "resolved": "https://registry.npmmirror.com/tldts/-/tldts-7.4.2.tgz", + "integrity": "sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.2" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.2", + "resolved": "https://registry.npmmirror.com/tldts-core/-/tldts-core-7.4.2.tgz", + "integrity": "sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==", + "dev": true, + "license": "MIT" + }, "node_modules/tmp": { "version": "0.0.28", "resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.0.28.tgz", @@ -20057,39 +22004,29 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.4", + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" + "tldts": "^7.0.5" }, "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 4.0.0" + "node": ">=16" } }, "node_modules/tr46": { - "version": "2.1.0", + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "punycode": "^2.1.1" + "punycode": "^2.3.1" }, "engines": { - "node": ">=8" + "node": ">=20" } }, "node_modules/tree-kit": { @@ -20291,6 +22228,16 @@ "through": "^2.3.8" } }, + "node_modules/undici": { + "version": "7.27.2", + "resolved": "https://registry.npmmirror.com/undici/-/undici-7.27.2.tgz", + "integrity": "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "7.24.6", "dev": true, @@ -20502,6 +22449,8 @@ }, "node_modules/url-parse": { "version": "1.5.10", + "resolved": "https://registry.npmmirror.com/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", "dev": true, "license": "MIT", "peer": true, @@ -21125,6 +23074,676 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/vitest": { + "version": "4.1.8", + "resolved": "https://registry.npmmirror.com/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.1.8", + "resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.8", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "node_modules/vitest/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmmirror.com/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/vue": { "version": "3.5.34", "license": "MIT", @@ -21144,6 +23763,13 @@ } } }, + "node_modules/vue-component-type-helpers": { + "version": "3.3.4", + "resolved": "https://registry.npmmirror.com/vue-component-type-helpers/-/vue-component-type-helpers-3.3.4.tgz", + "integrity": "sha512-joip1uZTaQR0nD23N400gIdJ7xY+WiiiMA/BCKz842gvGBknqDQAzklUvDEhqFvvrhQY8S2ZANBMu4X70VMFGw==", + "dev": true, + "license": "MIT" + }, "node_modules/vue-demi": { "version": "0.14.10", "hasInstallScript": true, @@ -21226,6 +23852,9 @@ }, "node_modules/w3c-hr-time": { "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "deprecated": "Use your platform's native performance.now() and performance.timeOrigin.", "dev": true, "license": "MIT", "peer": true, @@ -21234,15 +23863,16 @@ } }, "node_modules/w3c-xmlserializer": { - "version": "2.0.0", + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "xml-name-validator": "^3.0.0" + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/walker": { @@ -21265,12 +23895,13 @@ } }, "node_modules/webidl-conversions": { - "version": "6.1.0", + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { - "node": ">=10.4" + "node": ">=20" } }, "node_modules/webpack-virtual-modules": { @@ -21279,6 +23910,8 @@ }, "node_modules/whatwg-encoding": { "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", "dev": true, "license": "MIT", "peer": true, @@ -21287,23 +23920,28 @@ } }, "node_modules/whatwg-mimetype": { - "version": "2.3.0", + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", "dev": true, "license": "MIT", - "peer": true + "engines": { + "node": ">=20" + } }, "node_modules/whatwg-url": { - "version": "8.7.0", + "version": "16.0.1", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "lodash": "^4.7.0", - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" }, "engines": { - "node": ">=10" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/which": { @@ -21349,6 +23987,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", @@ -21377,6 +24032,25 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "dev": true, @@ -21434,10 +24108,14 @@ } }, "node_modules/xml-name-validator": { - "version": "3.0.0", + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, "license": "Apache-2.0", - "peer": true + "engines": { + "node": ">=18" + } }, "node_modules/xml-parse-from-string": { "version": "1.0.1", @@ -21463,9 +24141,10 @@ }, "node_modules/xmlchars": { "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/xmlhttprequest": { "version": "1.8.0", diff --git a/zhiyin-app/package.json b/zhiyin-app/package.json index d16df17..9e9c5bf 100644 --- a/zhiyin-app/package.json +++ b/zhiyin-app/package.json @@ -5,7 +5,9 @@ "dev:mp-weixin": "uni -p mp-weixin", "build:mp-weixin": "uni build -p mp-weixin", "dev:h5": "uni", - "build:h5": "uni build" + "build:h5": "uni build", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "@dcloudio/uni-app": "3.0.0-4060620250520001", @@ -23,9 +25,12 @@ "@dcloudio/uni-automator": "3.0.0-4060620250520001", "@dcloudio/uni-cli-shared": "3.0.0-4060620250520001", "@dcloudio/vite-plugin-uni": "3.0.0-4060620250520001", + "@vue/test-utils": "^2.4.11", + "jsdom": "^29.1.1", "miniprogram-ci": "^2.1.31", "sass": "^1.70.0", "typescript": "^5.3.0", - "vite": "^5.2.0" + "vite": "^5.2.0", + "vitest": "^4.1.8" } } diff --git a/zhiyin-app/src/config.spec.ts b/zhiyin-app/src/config.spec.ts new file mode 100644 index 0000000..2920f92 --- /dev/null +++ b/zhiyin-app/src/config.spec.ts @@ -0,0 +1,51 @@ +import { describe, it, expect } from 'vitest' + +describe('APP_CONFIG', () => { + it('should have APP_NAME', async () => { + const { APP_CONFIG } = await import('./config') + expect(APP_CONFIG.APP_NAME).toBeTruthy() + }) + + it('should have storage keys', async () => { + const { APP_CONFIG } = await import('./config') + expect(APP_CONFIG.STORAGE_KEYS.TOKEN).toBe('token') + expect(APP_CONFIG.STORAGE_KEYS.USER_ID).toBe('userId') + }) + + it('should have page routes', async () => { + const { APP_CONFIG } = await import('./config') + expect(APP_CONFIG.PAGES.INDEX).toBe('/pages/index/index') + expect(APP_CONFIG.PAGES.LOGIN).toBe('/pages/login/login') + expect(APP_CONFIG.PAGES.MEMBER).toBe('/pages/member/member') + }) +}) + +describe('API_ENDPOINTS', () => { + it('should have all endpoint groups', async () => { + const { API_ENDPOINTS } = await import('./config') + expect(API_ENDPOINTS.USER).toBeDefined() + expect(API_ENDPOINTS.INTERVIEW).toBeDefined() + expect(API_ENDPOINTS.PAYMENT).toBeDefined() + expect(API_ENDPOINTS.PROGRESS).toBeDefined() + expect(API_ENDPOINTS.CONTRIBUTION).toBeDefined() + expect(API_ENDPOINTS.MEMBER).toBeDefined() + }) + + it('should have user endpoints', async () => { + const { API_ENDPOINTS } = await import('./config') + expect(API_ENDPOINTS.USER.SEND_CODE).toBe('/user/send-code') + expect(API_ENDPOINTS.USER.LOGIN).toBe('/user/login') + expect(API_ENDPOINTS.USER.INFO).toBe('/user/info') + }) + + it('should generate dynamic interview endpoints', async () => { + const { API_ENDPOINTS } = await import('./config') + expect(API_ENDPOINTS.INTERVIEW.ANSWER('abc')).toBe('/interview/abc/answer') + expect(API_ENDPOINTS.INTERVIEW.GET('xyz')).toBe('/interview/xyz') + }) + + it('should generate dynamic payment check endpoint', async () => { + const { API_ENDPOINTS } = await import('./config') + expect(API_ENDPOINTS.PAYMENT.CHECK('ORD123')).toBe('/payment/check/ORD123') + }) +}) diff --git a/zhiyin-app/src/pages/report/report.vue b/zhiyin-app/src/pages/report/report.vue index 539b89c..611581a 100644 --- a/zhiyin-app/src/pages/report/report.vue +++ b/zhiyin-app/src/pages/report/report.vue @@ -24,6 +24,21 @@ {{ report.questionCount }} 题 + + 📊 四维能力评估 + + + + {{ dim.label }} + {{ dim.value }}分 + + + + + + + + 📝 评估总结 {{ report.summary }} @@ -40,22 +55,36 @@ - - + + + 暂无报告数据 + + + diff --git a/zhiyin-app/vitest.config.ts b/zhiyin-app/vitest.config.ts new file mode 100644 index 0000000..052ce3d --- /dev/null +++ b/zhiyin-app/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + environment: 'jsdom', + globals: true, + include: ['src/**/*.{test,spec}.{ts,js}'], + }, +})