diff --git a/.trae/documents/deearthx_improvement_plan.md b/.trae/documents/deearthx_improvement_plan.md index b4b960d..8ff5018 100644 --- a/.trae/documents/deearthx_improvement_plan.md +++ b/.trae/documents/deearthx_improvement_plan.md @@ -3,115 +3,210 @@ ## 项目分析 DeEarthX-CE 是一个与 Minecraft 相关的工具,包含以下组件: -- **后端**:TypeScript + Express 构建,提供模组检测、过滤、平台集成等功能 -- **前端**:Vue 3 + Ant Design Vue + Tauri 构建的桌面应用 -- **文档**:VitePress 构建的文档网站 + +* **后端**:TypeScript + Express 构建,提供模组检测、过滤、平台集成等功能 + +* **前端**:Vue 3 + Ant Design Vue + Tauri 构建的桌面应用 + +* **文档**:VitePress 构建的文档网站 ## 改进任务列表 -### [ ] 任务 1:代码质量检查与优化 -- **Priority**:P1 -- **Depends On**:None -- **Description**: - - 检查并清理未使用的依赖 - - 统一代码风格和命名规范 - - 优化错误处理机制 - - 提高代码可读性和可维护性 -- **Success Criteria**: - - 所有依赖都是必要的 - - 代码风格统一 - - 错误处理完善 -- **Test Requirements**: - - `programmatic` TR-1.1:运行 `npm run build` 无错误 - - `programmatic` TR-1.2:运行代码检查工具无严重警告 - - `human-judgement` TR-1.3:代码结构清晰,注释完善 +### \[ ] 任务 1:代码质量检查与优化 -### [ ] 任务 2:性能优化 -- **Priority**:P2 -- **Depends On**:任务 1 -- **Description**: - - 优化文件操作性能 - - 优化网络请求和响应 - - 减少不必要的计算和重复操作 - - 提高模组处理速度 -- **Success Criteria**: - - 文件操作速度提升 - - 网络请求响应时间减少 - - 模组处理效率提高 -- **Test Requirements**: - - `programmatic` TR-2.1:模组处理时间减少 20% - - `programmatic` TR-2.2:内存使用降低 15% - - `human-judgement` TR-2.3:用户操作响应更流畅 +* **Priority**:P1 -### [ ] 任务 3:安全性增强 -- **Priority**:P1 -- **Depends On**:任务 1 -- **Description**: - - 检查并修复安全漏洞 - - 加强输入验证 - - 优化文件操作安全性 - - 检查依赖的安全状态 -- **Success Criteria**: - - 无安全漏洞 - - 输入验证完善 - - 依赖无安全问题 -- **Test Requirements**: - - `programmatic` TR-3.1:运行安全扫描工具无严重漏洞 - - `programmatic` TR-3.2:所有输入都经过验证 - - `human-judgement` TR-3.3:安全措施到位 +* **Depends On**:None -### [ ] 任务 4:功能增强 -- **Priority**:P2 -- **Depends On**:任务 1, 任务 3 -- **Description**: - - 完善用户界面交互 - - 增加更多模组平台支持 - - 优化模板管理功能 - - 增强多语言支持 -- **Success Criteria**: - - 用户界面更友好 - - 支持更多模组平台 - - 模板管理更便捷 - - 多语言支持更完善 -- **Test Requirements**: - - `programmatic` TR-4.1:所有新增功能正常工作 - - `human-judgement` TR-4.2:用户界面美观易用 - - `human-judgement` TR-4.3:多语言支持准确 +* **Description**: -### [ ] 任务 5:构建和部署优化 -- **Priority**:P2 -- **Depends On**:任务 1, 任务 2 -- **Description**: - - 优化构建流程 - - 减少构建时间 - - 优化打包大小 - - 完善部署文档 -- **Success Criteria**: - - 构建流程更高效 - - 构建时间减少 - - 打包大小优化 - - 部署文档完善 -- **Test Requirements**: - - `programmatic` TR-5.1:构建时间减少 25% - - `programmatic` TR-5.2:打包大小减少 20% - - `human-judgement` TR-5.3:部署文档清晰完整 + * 检查并清理未使用的依赖 -### [ ] 任务 6:测试覆盖度提升 -- **Priority**:P3 -- **Depends On**:任务 1 -- **Description**: - - 增加单元测试 - - 增加集成测试 - - 提高测试覆盖度 - - 建立测试自动化流程 -- **Success Criteria**: - - 测试覆盖度达到 80% 以上 - - 关键功能有测试用例 - - 测试自动化流程建立 -- **Test Requirements**: - - `programmatic` TR-6.1:测试覆盖度达到 80% 以上 - - `programmatic` TR-6.2:所有测试用例通过 - - `human-judgement` TR-6.3:测试用例设计合理 + * 统一代码风格和命名规范 + + * 优化错误处理机制 + + * 提高代码可读性和可维护性 + +* **Success Criteria**: + + * 所有依赖都是必要的 + + * 代码风格统一 + + * 错误处理完善 + +* **Test Requirements**: + + * `programmatic` TR-1.1:运行 `npm run build` 无错误 + + * `programmatic` TR-1.2:运行代码检查工具无严重警告 + + * `human-judgement` TR-1.3:代码结构清晰,注释完善 + +### \[ ] 任务 2:性能优化 + +* **Priority**:P2 + +* **Depends On**:任务 1 + +* **Description**: + + * 优化文件操作性能 + + * 优化网络请求和响应 + + * 减少不必要的计算和重复操作 + + * 提高模组处理速度 + +* **Success Criteria**: + + * 文件操作速度提升 + + * 网络请求响应时间减少 + + * 模组处理效率提高 + +* **Test Requirements**: + + * `programmatic` TR-2.1:模组处理时间减少 20% + + * `programmatic` TR-2.2:内存使用降低 15% + + * `human-judgement` TR-2.3:用户操作响应更流畅 + +### \[ ] 任务 3:安全性增强 + +* **Priority**:P1 + +* **Depends On**:任务 1 + +* **Description**: + + * 检查并修复安全漏洞 + + * 加强输入验证 + + * 优化文件操作安全性 + + * 检查依赖的安全状态 + +* **Success Criteria**: + + * 无安全漏洞 + + * 输入验证完善 + + * 依赖无安全问题 + +* **Test Requirements**: + + * `programmatic` TR-3.1:运行安全扫描工具无严重漏洞 + + * `programmatic` TR-3.2:所有输入都经过验证 + + * `human-judgement` TR-3.3:安全措施到位 + +### \[ ] 任务 4:功能增强 + +* **Priority**:P2 + +* **Depends On**:任务 1, 任务 3 + +* **Description**: + + * 完善用户界面交互 + + * 增加更多模组平台支持 + + * 优化模板管理功能 + + * 增强多语言支持 + +* **Success Criteria**: + + * 用户界面更友好 + + * 支持更多模组平台 + + * 模板管理更便捷 + + * 多语言支持更完善 + +* **Test Requirements**: + + * `programmatic` TR-4.1:所有新增功能正常工作 + + * `human-judgement` TR-4.2:用户界面美观易用 + + * `human-judgement` TR-4.3:多语言支持准确 + +### \[ ] 任务 5:构建和部署优化 + +* **Priority**:P2 + +* **Depends On**:任务 1, 任务 2 + +* **Description**: + + * 优化构建流程 + + * 减少构建时间 + + * 优化打包大小 + + * 完善部署文档 + +* **Success Criteria**: + + * 构建流程更高效 + + * 构建时间减少 + + * 打包大小优化 + + * 部署文档完善 + +* **Test Requirements**: + + * `programmatic` TR-5.1:构建时间减少 25% + + * `programmatic` TR-5.2:打包大小减少 20% + + * `human-judgement` TR-5.3:部署文档清晰完整 + +### \[ ] 任务 6:测试覆盖度提升 + +* **Priority**:P3 + +* **Depends On**:任务 1 + +* **Description**: + + * 增加单元测试 + + * 增加集成测试 + + * 提高测试覆盖度 + + * 建立测试自动化流程 + +* **Success Criteria**: + + * 测试覆盖度达到 80% 以上 + + * 关键功能有测试用例 + + * 测试自动化流程建立 + +* **Test Requirements**: + + * `programmatic` TR-6.1:测试覆盖度达到 80% 以上 + + * `programmatic` TR-6.2:所有测试用例通过 + + * `human-judgement` TR-6.3:测试用例设计合理 ## 实施步骤 @@ -125,9 +220,16 @@ DeEarthX-CE 是一个与 Minecraft 相关的工具,包含以下组件: ## 预期成果 通过以上改进,DeEarthX-CE 项目将: -- 代码质量更高,更易维护 -- 性能更优,响应更快 -- 安全性更强,更可靠 -- 功能更完善,用户体验更好 -- 构建和部署更高效 -- 测试覆盖更全面,质量更有保障 \ No newline at end of file + +* 代码质量更高,更易维护 + +* 性能更优,响应更快 + +* 安全性更强,更可靠 + +* 功能更完善,用户体验更好 + +* 构建和部署更高效 + +* 测试覆盖更全面,质量更有保障 + diff --git a/backend/src/dearth/ModCheckService.ts b/backend/src/dearth/ModCheckService.ts index 6ace7f5..97369fc 100644 --- a/backend/src/dearth/ModCheckService.ts +++ b/backend/src/dearth/ModCheckService.ts @@ -550,12 +550,12 @@ export class ModCheckService { jarData = fs.readFileSync(file.filename); } - const { Azip } = await import("../utils/ziplib.js"); - const zipEntries = Azip(jarData); + const { yauzl_promise } = await import("../utils/ziplib.js"); + const zipEntries = await yauzl_promise(jarData); for (const entry of zipEntries) { - if (entry.entryName === iconPath || entry.entryName.endsWith(iconPath)) { - const data = await entry.getData(); + if (entry.fileName === iconPath || entry.fileName.endsWith(iconPath)) { + const data = await entry.ReadEntry; const ext = iconPath.split('.').pop()?.toLowerCase(); const mimeType = ext === 'png' ? 'png' : 'jpeg'; diff --git a/backend/src/dearth/utils/FileExtractor.ts b/backend/src/dearth/utils/FileExtractor.ts index da9547f..3380eb4 100644 --- a/backend/src/dearth/utils/FileExtractor.ts +++ b/backend/src/dearth/utils/FileExtractor.ts @@ -22,23 +22,18 @@ export class FileExtractor { const fullPath = path.join(this.modsPath, jarFilename); try { - let fileData: Buffer | null = null; - try { - fileData = fs.readFileSync(fullPath); - const mixins = await JarParser.extractMixins(fileData); - const infos = await JarParser.extractModInfo(fileData); + const hash = await this.calculateFileHash(fullPath); + const mixins = await JarParser.extractMixinsFromFile(fullPath); + const infos = await JarParser.extractModInfoFromFile(fullPath); - files.push({ - filename: fullPath, - hash: crypto.createHash('sha1').update(fileData).digest('hex'), - mixins, - infos, - }); + files.push({ + filename: fullPath, + hash, + mixins, + infos, + }); - logger.debug("文件已处理", { 文件名: fullPath, 绝对路径: path.resolve(fullPath), Mixin数量: mixins.length }); - } finally { - fileData = null; - } + logger.debug("文件已处理", { 文件名: fullPath, 绝对路径: path.resolve(fullPath), Mixin数量: mixins.length }); } catch (error: any) { logger.error("处理文件时出错", { 文件名: fullPath, 错误: error.message }); } @@ -48,6 +43,25 @@ export class FileExtractor { return files; } + private async calculateFileHash(filePath: string): Promise { + return new Promise((resolve, reject) => { + const hash = crypto.createHash('sha1'); + const stream = fs.createReadStream(filePath); + + stream.on('data', (chunk) => { + hash.update(chunk); + }); + + stream.on('end', () => { + resolve(hash.digest('hex')); + }); + + stream.on('error', (error) => { + reject(error); + }); + }); + } + private getJarFiles(): string[] { if (!fs.existsSync(this.modsPath)) { fs.mkdirSync(this.modsPath, { recursive: true }); diff --git a/backend/src/modloader/forge.ts b/backend/src/modloader/forge.ts index efa1655..1841acc 100644 --- a/backend/src/modloader/forge.ts +++ b/backend/src/modloader/forge.ts @@ -2,7 +2,7 @@ import got, { Got } from "got"; import fs from "node:fs"; import fse from "fs-extra"; import { execPromise, fastdownload, version_compare, verifySHA1 } from "../utils/utils.js"; -import { Azip } from "../utils/ziplib.js"; +import { yauzl_promise } from "../utils/ziplib.js"; import { Config } from "../utils/config.js"; import { logger } from "../utils/logger.js"; @@ -84,18 +84,20 @@ export class Forge { async library() { const _downlist: [string, string][] = []; const data = await fs.promises.readFile(`${this.path}/forge-${this.minecraft}-${this.loaderVersion}-installer.jar`); - const zip = Azip(data); + const zip = await yauzl_promise(data); - for await (const entry of zip) { - if (entry.entryName === "version.json" || entry.entryName === "install_profile.json") { - JSON.parse((entry.getData()).toString()).libraries.forEach(async (e: any) => { + for (const entry of zip) { + if (entry.fileName === "version.json" || entry.fileName === "install_profile.json") { + const entryData = await entry.ReadEntry; + JSON.parse(entryData.toString()).libraries.forEach(async (e: any) => { const t = e.downloads.artifact.path; _downlist.push([`https://bmclapi2.bangbang93.com/maven/${t}`, `${this.path}/libraries/${t}`]); }); } - if (entry.entryName === "install_profile.json") { - const json = JSON.parse((entry.getData()).toString()) as IForge; + if (entry.fileName === "install_profile.json") { + const entryData = await entry.ReadEntry; + const json = JSON.parse(entryData.toString()) as IForge; const vjson = await this.got.get(`version/${this.minecraft}/json`).json(); console.log(`${new URL(vjson.downloads.server_mappings.url).pathname}`); const mojpath = this.MTP(json.data.MOJMAPS.server); diff --git a/backend/src/modloader/minecraft.ts b/backend/src/modloader/minecraft.ts index 53e39a4..fb8a85a 100644 --- a/backend/src/modloader/minecraft.ts +++ b/backend/src/modloader/minecraft.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import { fastdownload, version_compare } from "../utils/utils.js"; import got from "got"; import p from "path"; -import { Azip } from "../utils/ziplib.js"; +import { yauzl_promise } from "../utils/ziplib.js"; import { Config } from "../utils/config.js"; interface ILInfo { @@ -54,12 +54,12 @@ export class Minecraft { if (version_compare(this.minecraft, "1.18") === 1) { const mcpath = `${this.path}/libraries/net/minecraft/server/${this.minecraft}/server-${this.minecraft}.jar`; await fastdownload([`https://bmclapi2.bangbang93.com/version/${this.minecraft}/server`, mcpath]); - const zip = await Azip(await fs.promises.readFile(mcpath)); - for await (const entry of zip) { - if (entry.entryName.startsWith("META-INF/libraries/") && !entry.entryName.endsWith("/")) { - console.log(entry.entryName); - const data = entry.getData(); - const filepath = `${this.path}/libraries/${entry.entryName.replace("META-INF/libraries/", "")}`; + const zip = await yauzl_promise(await fs.promises.readFile(mcpath)); + for (const entry of zip) { + if (entry.fileName.startsWith("META-INF/libraries/") && !entry.fileName.endsWith("/")) { + console.log(entry.fileName); + const data = await entry.ReadEntry; + const filepath = `${this.path}/libraries/${entry.fileName.replace("META-INF/libraries/", "")}`; const dir = p.dirname(filepath); await fs.promises.mkdir(dir, { recursive: true }); await fs.promises.writeFile(filepath, data); diff --git a/backend/src/utils/jar-parser.ts b/backend/src/utils/jar-parser.ts index 6a6fae8..b52316f 100644 --- a/backend/src/utils/jar-parser.ts +++ b/backend/src/utils/jar-parser.ts @@ -1,20 +1,124 @@ import { IInfoFile, IMixinFile } from "../dearth/types.js"; -import { Azip } from "./ziplib.js"; +import { yauzl_promise } from "./ziplib.js"; import toml from "smol-toml"; +import fs from "node:fs"; +import yauzl from "yauzl"; export class JarParser { static async extractModInfo(jarData: Buffer): Promise { + return this.extractModInfoFromBuffer(jarData); + } + + static async extractMixins(jarData: Buffer): Promise { + return this.extractMixinsFromBuffer(jarData); + } + + static async extractModInfoFromFile(filePath: string): Promise { + return new Promise((resolve, reject) => { + const infos: IInfoFile[] = []; + + yauzl.open(filePath, (err, zipfile) => { + if (err) { + reject(err); + return; + } + + zipfile.on("entry", (entry) => { + if (entry.fileName.endsWith("neoforge.mods.toml") || entry.fileName.endsWith("mods.toml") || entry.fileName.endsWith("fabric.mod.json")) { + zipfile.openReadStream(entry, (err, stream) => { + if (err) { + return; + } + + const chunks: Buffer[] = []; + stream.on("data", (chunk) => { + chunks.push(chunk); + }); + + stream.on("end", () => { + try { + const data = Buffer.concat(chunks); + if (entry.fileName.endsWith(".toml")) { + infos.push({ name: entry.fileName, data: JSON.stringify(toml.parse(data.toString())) }); + } else if (entry.fileName.endsWith(".json")) { + infos.push({ name: entry.fileName, data: data.toString() }); + } + } catch (error) { + // 忽略解析错误 + } + }); + }); + } + }); + + zipfile.on("end", () => { + resolve(infos); + }); + + zipfile.on("error", (err) => { + reject(err); + }); + }); + }); + } + + static async extractMixinsFromFile(filePath: string): Promise { + return new Promise((resolve, reject) => { + const mixins: IMixinFile[] = []; + + yauzl.open(filePath, (err, zipfile) => { + if (err) { + reject(err); + return; + } + + zipfile.on("entry", (entry) => { + if (entry.fileName.endsWith(".mixins.json") && !entry.fileName.includes("/")) { + zipfile.openReadStream(entry, (err, stream) => { + if (err) { + return; + } + + const chunks: Buffer[] = []; + stream.on("data", (chunk) => { + chunks.push(chunk); + }); + + stream.on("end", () => { + try { + const data = Buffer.concat(chunks); + mixins.push({ name: entry.fileName, data: data.toString() }); + } catch (error) { + // 忽略解析错误 + } + }); + }); + } + }); + + zipfile.on("end", () => { + resolve(mixins); + }); + + zipfile.on("error", (err) => { + reject(err); + }); + }); + }); + } + + private static async extractModInfoFromBuffer(jarData: Buffer): Promise { const infos: IInfoFile[] = []; - const zipEntries = Azip(jarData); + const zipEntries = await yauzl_promise(jarData); for (const entry of zipEntries) { try { - if (entry.entryName.endsWith("neoforge.mods.toml") || entry.entryName.endsWith("mods.toml")) { - const data = await entry.getData(); - infos.push({ name: entry.entryName, data: JSON.stringify(toml.parse(data.toString())) }); - } else if (entry.entryName.endsWith("fabric.mod.json")) { - const data = await entry.getData(); - infos.push({ name: entry.entryName, data: data.toString() }); + if (entry.fileName.endsWith("neoforge.mods.toml") || entry.fileName.endsWith("mods.toml")) { + const data = await entry.ReadEntry; + infos.push({ name: entry.fileName, data: JSON.stringify(toml.parse(data.toString())) }); + } else if (entry.fileName.endsWith("fabric.mod.json")) { + const data = await entry.ReadEntry; + infos.push({ name: entry.fileName, data: data.toString() }); } } catch (error: any) { continue; @@ -24,15 +128,15 @@ export class JarParser { return infos; } - static async extractMixins(jarData: Buffer): Promise { + private static async extractMixinsFromBuffer(jarData: Buffer): Promise { const mixins: IMixinFile[] = []; - const zipEntries = Azip(jarData); + const zipEntries = await yauzl_promise(jarData); for (const entry of zipEntries) { - if (entry.entryName.endsWith(".mixins.json") && !entry.entryName.includes("/")) { + if (entry.fileName.endsWith(".mixins.json") && !entry.fileName.includes("/")) { try { - const data = await entry.getData(); - mixins.push({ name: entry.entryName, data: data.toString() }); + const data = await entry.ReadEntry; + mixins.push({ name: entry.fileName, data: data.toString() }); } catch (error: any) { continue; } diff --git a/backend/src/utils/ziplib.ts b/backend/src/utils/ziplib.ts index e7c68dd..f7f4067 100644 --- a/backend/src/utils/ziplib.ts +++ b/backend/src/utils/ziplib.ts @@ -1,4 +1,3 @@ -import admZip from "adm-zip"; import yauzl from "yauzl"; import Stream from "node:stream"; @@ -11,7 +10,7 @@ export async function yauzl_promise(buffer: Buffer): Promise { const zip = await (new Promise((resolve, reject) => { yauzl.fromBuffer( buffer, - /*{lazyEntries:true},*/ (err, zipfile) => { + (err, zipfile) => { if (err) { reject(err); return; @@ -43,6 +42,9 @@ export async function yauzl_promise(buffer: Buffer): Promise { stream.on("end", () => { resolve(Buffer.concat(chunks)); }); + stream.on("error", (err) => { + reject(err); + }); }); }); }; @@ -64,16 +66,15 @@ export async function yauzl_promise(buffer: Buffer): Promise { return new Promise((resolve, reject) => { const entries: IentryP[] = []; - zip.on("entry", async (entry: yauzl.Entry) => { - const entryP = entry as IentryP; - //console.log(entry.fileName); - entryP.openReadStream = _openReadStream(zip, entry); - entryP.ReadEntry = _ReadEntry(zip, entry); - entries.push(entryP); - if (zip.entryCount === entries.length) { - zip.close(); - resolve(entries); - } + zip.on("entry", (entry: yauzl.Entry) => { + const entryP = entry as IentryP; + entryP.openReadStream = _openReadStream(zip, entry); + entryP.ReadEntry = _ReadEntry(zip, entry); + entries.push(entryP); + }); + zip.on("end", () => { + zip.close(); + resolve(entries); }); zip.on("error", (err) => { reject(err); @@ -81,8 +82,15 @@ export async function yauzl_promise(buffer: Buffer): Promise { }); } -export function Azip(buffer: Buffer) { - const zip = new admZip(buffer); - const entries = zip.getEntries(); - return entries; +// 从文件路径打开 zip 文件的函数 +export async function yauzl_from_file(filePath: string): Promise { + return new Promise((resolve, reject) => { + yauzl.open(filePath, { lazyEntries: true }, (err, zipfile) => { + if (err) { + reject(err); + return; + } + resolve(zipfile); + }); + }); } diff --git a/front/package.json b/front/package.json index f2186df..6c8ac8c 100644 --- a/front/package.json +++ b/front/package.json @@ -34,6 +34,7 @@ "@vitejs/plugin-vue": "^5.2.4", "typescript": "~5.6.3", "vite": "^6.4.1", - "vue-tsc": "^2.2.12" + "vue-tsc": "^2.2.12", + "tslib": "^2.8.1" } } diff --git a/front/public/version.json b/front/public/version.json index e19787d..9e2cec0 100644 --- a/front/public/version.json +++ b/front/public/version.json @@ -1,5 +1,5 @@ { - "version": "3.0.35", - "buildTime": "2026-03-10", - "author": "DeEarthX Team" + "version": "3.0.36", + "buildTime": "2026-03-15", + "author": "xcclyc" } diff --git a/front/src/views/AboutView.vue b/front/src/views/AboutView.vue index 7b82f10..8871445 100644 --- a/front/src/views/AboutView.vue +++ b/front/src/views/AboutView.vue @@ -36,7 +36,7 @@ async function getVersionFromJson(): Promise { return { version: '1.0.0', buildTime: 'Unknown', - author: 'Tianpao' + author: 'DeEarthX Team' }; } } diff --git a/package.json b/package.json index f971ecc..c1f1feb 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "main", "repository": { "type": "git", - "url": "https://git.xcclyc.cn/15736060610/DeEarthX-CE.git" + "url": "https://git.xcclyc.cn/xcclyc/DeEarthX-CE.git" }, "license": "ISC", "author": "DexV3 Team", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1be0532..30ff91e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -166,6 +166,9 @@ importers: '@vitejs/plugin-vue': specifier: ^5.2.4 version: 5.2.4(vite@6.4.1(@types/node@25.3.5)(jiti@2.6.1)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0))(vue@3.5.29(typescript@5.6.3)) + tslib: + specifier: ^2.8.1 + version: 2.8.1 typescript: specifier: ~5.6.3 version: 5.6.3 @@ -6593,8 +6596,7 @@ snapshots: babel-jest: 30.2.0(@babel/core@7.29.0) jest-util: 30.2.0 - tslib@2.8.1: - optional: true + tslib@2.8.1: {} type-detect@4.0.8: {}