diff --git a/package.json b/package.json index 478ecf6..30ab7f6 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "eslint-config-prettier": "^10.1.8", "globals": "^17.4.0", "happy-dom": "^20.8.4", - "p-queue": "^8.1.1", + "p-queue": "^9.3.0", "prettier": "3.6.2", "tsup": "^8.5.1", "typescript-eslint": "^8.57.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 623181c..f22b0e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,14 +33,14 @@ importers: specifier: ^20.8.4 version: 20.8.4 p-queue: - specifier: ^8.1.1 - version: 8.1.1 + specifier: ^9.3.0 + version: 9.3.0 prettier: specifier: 3.6.2 version: 3.6.2 tsup: specifier: ^8.5.1 - version: 8.5.1(postcss@8.5.8)(typescript@5.9.2) + version: 8.5.1(postcss@8.5.15)(typescript@5.9.2) typescript-eslint: specifier: ^8.57.1 version: 8.57.1(eslint@10.0.3)(typescript@5.9.2) @@ -438,126 +438,251 @@ packages: cpu: [arm] os: [android] + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} + cpu: [arm] + os: [android] + '@rollup/rollup-android-arm64@4.59.0': resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} cpu: [arm64] os: [android] + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} + cpu: [arm64] + os: [android] + '@rollup/rollup-darwin-arm64@4.59.0': resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} cpu: [arm64] os: [darwin] + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} + cpu: [arm64] + os: [darwin] + '@rollup/rollup-darwin-x64@4.59.0': resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} cpu: [x64] os: [darwin] + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} + cpu: [x64] + os: [darwin] + '@rollup/rollup-freebsd-arm64@4.59.0': resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} cpu: [arm64] os: [freebsd] + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} + cpu: [arm64] + os: [freebsd] + '@rollup/rollup-freebsd-x64@4.59.0': resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} cpu: [x64] os: [freebsd] + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} + cpu: [x64] + os: [freebsd] + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} + cpu: [arm64] + os: [linux] + '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} + cpu: [arm64] + os: [linux] + '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} + cpu: [loong64] + os: [linux] + '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} + cpu: [loong64] + os: [linux] + '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} + cpu: [ppc64] + os: [linux] + '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} + cpu: [ppc64] + os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} + cpu: [riscv64] + os: [linux] + '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} + cpu: [riscv64] + os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} + cpu: [s390x] + os: [linux] + '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} + cpu: [x64] + os: [linux] + '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} + cpu: [x64] + os: [linux] + '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} cpu: [x64] os: [openbsd] + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} + cpu: [x64] + os: [openbsd] + '@rollup/rollup-openharmony-arm64@4.59.0': resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} cpu: [arm64] os: [openharmony] + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} + cpu: [arm64] + os: [openharmony] + '@rollup/rollup-win32-arm64-msvc@4.59.0': resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} cpu: [arm64] os: [win32] + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} + cpu: [arm64] + os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.59.0': resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} cpu: [ia32] os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} + cpu: [ia32] + os: [win32] + '@rollup/rollup-win32-x64-gnu@4.59.0': resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} cpu: [x64] os: [win32] + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} + cpu: [x64] + os: [win32] + '@rollup/rollup-win32-x64-msvc@4.59.0': resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} cpu: [x64] os: [win32] + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} + cpu: [x64] + os: [win32] + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -576,6 +701,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -962,8 +1090,8 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -989,13 +1117,13 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - p-queue@8.1.1: - resolution: {integrity: sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==} - engines: {node: '>=18'} + p-queue@9.3.0: + resolution: {integrity: sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==} + engines: {node: '>=20'} - p-timeout@6.1.4: - resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==} - engines: {node: '>=14.16'} + p-timeout@7.0.1: + resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==} + engines: {node: '>=20'} path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} @@ -1015,6 +1143,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pirates@4.0.7: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} @@ -1040,8 +1172,8 @@ packages: yaml: optional: true - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -1070,6 +1202,11 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} @@ -1126,6 +1263,10 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + tinyrainbow@3.1.0: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} @@ -1520,78 +1661,153 @@ snapshots: '@rollup/rollup-android-arm-eabi@4.59.0': optional: true + '@rollup/rollup-android-arm-eabi@4.62.2': + optional: true + '@rollup/rollup-android-arm64@4.59.0': optional: true + '@rollup/rollup-android-arm64@4.62.2': + optional: true + '@rollup/rollup-darwin-arm64@4.59.0': optional: true + '@rollup/rollup-darwin-arm64@4.62.2': + optional: true + '@rollup/rollup-darwin-x64@4.59.0': optional: true + '@rollup/rollup-darwin-x64@4.62.2': + optional: true + '@rollup/rollup-freebsd-arm64@4.59.0': optional: true + '@rollup/rollup-freebsd-arm64@4.62.2': + optional: true + '@rollup/rollup-freebsd-x64@4.59.0': optional: true + '@rollup/rollup-freebsd-x64@4.62.2': + optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + optional: true + '@rollup/rollup-linux-arm-musleabihf@4.59.0': optional: true + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + optional: true + '@rollup/rollup-linux-arm64-gnu@4.59.0': optional: true + '@rollup/rollup-linux-arm64-gnu@4.62.2': + optional: true + '@rollup/rollup-linux-arm64-musl@4.59.0': optional: true + '@rollup/rollup-linux-arm64-musl@4.62.2': + optional: true + '@rollup/rollup-linux-loong64-gnu@4.59.0': optional: true + '@rollup/rollup-linux-loong64-gnu@4.62.2': + optional: true + '@rollup/rollup-linux-loong64-musl@4.59.0': optional: true + '@rollup/rollup-linux-loong64-musl@4.62.2': + optional: true + '@rollup/rollup-linux-ppc64-gnu@4.59.0': optional: true + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + optional: true + '@rollup/rollup-linux-ppc64-musl@4.59.0': optional: true + '@rollup/rollup-linux-ppc64-musl@4.62.2': + optional: true + '@rollup/rollup-linux-riscv64-gnu@4.59.0': optional: true + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + optional: true + '@rollup/rollup-linux-riscv64-musl@4.59.0': optional: true + '@rollup/rollup-linux-riscv64-musl@4.62.2': + optional: true + '@rollup/rollup-linux-s390x-gnu@4.59.0': optional: true + '@rollup/rollup-linux-s390x-gnu@4.62.2': + optional: true + '@rollup/rollup-linux-x64-gnu@4.59.0': optional: true + '@rollup/rollup-linux-x64-gnu@4.62.2': + optional: true + '@rollup/rollup-linux-x64-musl@4.59.0': optional: true + '@rollup/rollup-linux-x64-musl@4.62.2': + optional: true + '@rollup/rollup-openbsd-x64@4.59.0': optional: true + '@rollup/rollup-openbsd-x64@4.62.2': + optional: true + '@rollup/rollup-openharmony-arm64@4.59.0': optional: true + '@rollup/rollup-openharmony-arm64@4.62.2': + optional: true + '@rollup/rollup-win32-arm64-msvc@4.59.0': optional: true + '@rollup/rollup-win32-arm64-msvc@4.62.2': + optional: true + '@rollup/rollup-win32-ia32-msvc@4.59.0': optional: true + '@rollup/rollup-win32-ia32-msvc@4.62.2': + optional: true + '@rollup/rollup-win32-x64-gnu@4.59.0': optional: true + '@rollup/rollup-win32-x64-gnu@4.62.2': + optional: true + '@rollup/rollup-win32-x64-msvc@4.59.0': optional: true + '@rollup/rollup-win32-x64-msvc@4.62.2': + optional: true + '@standard-schema/spec@1.1.0': {} '@types/adm-zip@0.5.8': @@ -1609,6 +1825,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/estree@1.0.9': {} + '@types/json-schema@7.0.15': {} '@types/node@25.5.0': @@ -1961,6 +2179,10 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -2068,7 +2290,7 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nanoid@3.3.11: {} + nanoid@3.3.15: {} natural-compare@1.4.0: {} @@ -2093,12 +2315,12 @@ snapshots: dependencies: p-limit: 3.1.0 - p-queue@8.1.1: + p-queue@9.3.0: dependencies: eventemitter3: 5.0.4 - p-timeout: 6.1.4 + p-timeout: 7.0.1 - p-timeout@6.1.4: {} + p-timeout@7.0.1: {} path-exists@4.0.0: {} @@ -2110,6 +2332,8 @@ snapshots: picomatch@4.0.3: {} + picomatch@4.0.4: {} + pirates@4.0.7: {} pkg-types@1.3.1: @@ -2118,15 +2342,15 @@ snapshots: mlly: 1.8.1 pathe: 2.0.3 - postcss-load-config@6.0.1(postcss@8.5.8): + postcss-load-config@6.0.1(postcss@8.5.15): dependencies: lilconfig: 3.1.3 optionalDependencies: - postcss: 8.5.8 + postcss: 8.5.15 - postcss@8.5.8: + postcss@8.5.15: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.15 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -2171,6 +2395,37 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 + rollup@4.62.2: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 + fsevents: 2.3.3 + semver@7.7.4: {} shebang-command@2.0.0: @@ -2218,6 +2473,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + tinyrainbow@3.1.0: {} tree-kill@1.2.2: {} @@ -2228,7 +2488,7 @@ snapshots: ts-interface-checker@0.1.13: {} - tsup@8.5.1(postcss@8.5.8)(typescript@5.9.2): + tsup@8.5.1(postcss@8.5.15)(typescript@5.9.2): dependencies: bundle-require: 5.1.0(esbuild@0.27.4) cac: 6.7.14 @@ -2239,7 +2499,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(postcss@8.5.8) + postcss-load-config: 6.0.1(postcss@8.5.15) resolve-from: 5.0.0 rollup: 4.59.0 source-map: 0.7.6 @@ -2248,7 +2508,7 @@ snapshots: tinyglobby: 0.2.15 tree-kill: 1.2.2 optionalDependencies: - postcss: 8.5.8 + postcss: 8.5.15 typescript: 5.9.2 transitivePeerDependencies: - jiti @@ -2286,11 +2546,11 @@ snapshots: vite@7.1.7(@types/node@25.5.0): dependencies: esbuild: 0.25.12 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.8 - rollup: 4.59.0 - tinyglobby: 0.2.15 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 + rollup: 4.62.2 + tinyglobby: 0.2.17 optionalDependencies: '@types/node': 25.5.0 fsevents: 2.3.3 diff --git a/src/core/arguments.ts b/src/core/arguments.ts index 25846e7..b85780a 100644 --- a/src/core/arguments.ts +++ b/src/core/arguments.ts @@ -1,6 +1,6 @@ import path from "path"; import { isNeeded } from "../utils/rules"; -import { Argument, Version, Config } from "../utils/types"; +import { Argument, Version, FinalConfig } from "../utils/types"; import { getLibraries } from "./libraries"; import { version as packageVersion } from "../../package.json"; import { getArgument } from "./log4j"; @@ -9,7 +9,7 @@ import { getArgument } from "./log4j"; function fillArguments( arg: string, versionManifest: Version, - config: Config, + config: FinalConfig, classPaths: string ) { const argumentsToFill: { [key: string]: string } = { @@ -51,7 +51,7 @@ function parseArg( this: string[], arg: Argument, versionManifest: Version, - config: Config, + config: FinalConfig, classPaths: string ) { if (isNeeded(arg)) { @@ -106,7 +106,7 @@ function generateClassPaths( export async function generateLaunchArguments( versionManifest: Version, - config: Config + config: FinalConfig ) { const jvm: string[] = []; let game: string[] = []; diff --git a/src/core/mergeManifests.ts b/src/core/mergeManifests.ts index 1be59f0..36ce14a 100644 --- a/src/core/mergeManifests.ts +++ b/src/core/mergeManifests.ts @@ -1,12 +1,12 @@ import path from "path"; import { readJson } from "../utils/fs"; -import { Version } from "../utils/types"; -import { getVersionId, ModloaderConfig } from "./modloaders"; +import { Version, ModloaderConfig } from "../utils/types"; +import { getVersionId } from "./modloaders"; function mergeManifests(base: Version, layer: Version) { // Copy needed properties to base // Those properties should be enough for forge at least - const result = { ...base } + const result = { ...base }; result.libraries = []; result.mainClass = layer.mainClass; @@ -24,24 +24,24 @@ function mergeManifests(base: Version, layer: Version) { // } for (const bLib of base.libraries) { - let isInLayer = false + let isInLayer = false; for (const lLib of layer.libraries) { - const lId = lLib.name.split(":")[1] - const bId = bLib.name.split(":")[1] + const lId = lLib.name.split(":")[1]; + const bId = bLib.name.split(":")[1]; if (lId === bId) { - isInLayer = true - break + isInLayer = true; + break; } } if (!isInLayer) { - result.libraries.push(bLib) + result.libraries.push(bLib); } } for (const lLib of layer.libraries) { - result.libraries.push(lLib) + result.libraries.push(lLib); } // modern versions @@ -59,26 +59,22 @@ function mergeManifests(base: Version, layer: Version) { } } else { // older versions - result.minecraftArguments = layer.minecraftArguments + result.minecraftArguments = layer.minecraftArguments; } return result; } -export async function prepareManifest(config: ModloaderConfig, versionManifest: Version) { - const versionId = getVersionId(config) +export async function prepareManifest( + config: ModloaderConfig, + versionManifest: Version, +) { + const versionId = getVersionId(config); const moddedVersionManifest = await readJson( - path.join( - config.paths.versions, - versionId, - `${versionId}.json`, - ), + path.join(config.paths.versions, versionId, `${versionId}.json`), ); - versionManifest = mergeManifests( - versionManifest!, - moddedVersionManifest, - ); + versionManifest = mergeManifests(versionManifest!, moddedVersionManifest); - return versionManifest -} \ No newline at end of file + return versionManifest; +} diff --git a/src/core/modrinth.ts b/src/core/modrinth.ts new file mode 100644 index 0000000..72e6c17 --- /dev/null +++ b/src/core/modrinth.ts @@ -0,0 +1,379 @@ +import path from "node:path"; +import { + CopyQueue, + DeleteQueue, + exists, + readJson, + TreeMakerQueue, +} from "../utils/fs"; +import { + FinalConfig, + ModrinthManifest, + ModrinthManifestFile, + PoolFile, +} from "../utils/types"; +import fs from "fs/promises"; +import { DownloadPool } from "../utils/fetch"; +import { Instance } from "../launcher"; + +export async function getProjectsChanges( + instanceFolder: string, + manifest: ModrinthManifest, + oldManifest?: ModrinthManifest, +): Promise<{ addedProjects: PoolFile[]; removedProject: string[] }> { + if (oldManifest) { + const newFiles = new Map(); + const oldFiles = new Map(); + + const addedProjects: PoolFile[] = []; + const removedProject: string[] = []; + + manifest.files.forEach((f) => { + const regex = /https:\/\/(.*)\/data\/(.*)\/versions\/(.*)\/(.*)/gm; + + const fileInfo = regex.exec(f.downloads[0])!; + + newFiles.set(fileInfo[2], f); + }); + + oldManifest.files.forEach((f) => { + const regex = /https:\/\/(.*)\/data\/(.*)\/versions\/(.*)\/(.*)/gm; + + const fileInfo = regex.exec(f.downloads[0])!; + + oldFiles.set(fileInfo[2], f); + }); + + for (const [fileID, newFile] of newFiles.entries()) { + if (!oldFiles.has(fileID)) { + // new project + addedProjects.push({ + url: newFile.downloads[0], + path: path.join(instanceFolder, newFile.path), + size: newFile.fileSize, + }); + } + } + + for (const [fileID, oldFile] of oldFiles.entries()) { + if (!newFiles.has(fileID)) { + // has been removed in update + removedProject.push(path.join(instanceFolder, oldFile.path)); + } else { + // same project + const newFile = newFiles.get(fileID)!; + if (oldFile.downloads[0] !== newFile.downloads[0]) { + // different version + // mark old file for deletion + // add new one + removedProject.push(path.join(instanceFolder, oldFile.path)); + addedProjects.push({ + url: newFile.downloads[0], + path: path.join(instanceFolder, newFile.path), + size: newFile.fileSize, + }); + } + } + } + + return { + addedProjects, + removedProject, + }; + } else { + return { + addedProjects: manifest.files.map((f) => { + return { + url: f.downloads[0], + path: path.join(instanceFolder, f.path), + size: f.fileSize, + }; + }), + removedProject: [], + }; + } +} + +export async function getOverridesChanges( + instanceFolder: string, + tempFolder: string, +): Promise<{ + addedOverrides: string[]; + removedOverrides: string[]; + addedDirs: string[]; + removedDirs: string[]; +}> { + const overridesPath = path.join(tempFolder, "pack", "overrides"); + const overridesList = await fs.readdir(overridesPath, { + recursive: true, + withFileTypes: true, + }); + + if (await exists(path.join(tempFolder, "oldPack", "overrides"))) { + const oldOverridesPath = path.join(tempFolder, "oldPack", "overrides"); + // relative path + const addedOverrides: string[] = []; + // absolute path + const removedOverrides: string[] = []; + const addedDirs: string[] = []; + const removedDirs: string[] = []; + + const oldOverridesList = await fs.readdir(oldOverridesPath, { + recursive: true, + withFileTypes: true, + }); + + oldOverridesList.forEach((f) => { + const relativePath = path.relative( + oldOverridesPath, + path.join(f.parentPath, f.name), + ); + if ( + overridesList.find( + (a) => + path.relative(overridesPath, path.join(a.parentPath, a.name)) === + relativePath, + ) + ) { + // overwrite as it may be a different file + if (f.isDirectory()) { + removedDirs.push(path.join(instanceFolder, relativePath)); + addedDirs.push(path.join(instanceFolder, relativePath)); + } else { + removedOverrides.push(path.join(instanceFolder, relativePath)); + addedOverrides.push(relativePath); + } + } else { + // delete as it is no longer in the new version + if (f.isDirectory()) { + removedDirs.push(path.join(instanceFolder, relativePath)); + } else { + removedOverrides.push(path.join(instanceFolder, relativePath)); + } + } + }); + + overridesList.forEach((newFile) => { + const relativePath = path.relative( + overridesPath, + path.join(newFile.parentPath, newFile.name), + ); + + if ( + !oldOverridesList.find( + (oldFile) => + relativePath === + path.relative( + oldOverridesPath, + path.join(oldFile.parentPath, oldFile.name), + ), + ) + ) { + // new file + if (!newFile.isDirectory()) { + addedOverrides.push(relativePath); + } + } + }); + + return { addedOverrides, removedOverrides, addedDirs, removedDirs }; + } + + const addedDirs: string[] = []; + const addedOverrides: string[] = []; + + for (const file of overridesList) { + if (file.isDirectory()) { + addedDirs.push( + path.relative(overridesPath, path.join(file.parentPath, file.name)), + ); + } else + addedOverrides.push( + path.relative(overridesPath, path.join(file.parentPath, file.name)), + ); + } + + return { + addedOverrides, + removedOverrides: [], + addedDirs, + removedDirs: [], + }; +} + +export async function generatePackChanges( + instanceFolder: string, + tempFolder: string, + config: FinalConfig, +) { + const manifestPath = path.join(tempFolder, "pack", "modrinth.index.json"); + const manifest = await readJson(manifestPath); + let oldManifest: ModrinthManifest | undefined; + + if (config.modrinth?.updateFrom) { + const oldManifestPath = path.join( + tempFolder, + "oldPack", + "modrinth.index.json", + ); + oldManifest = await readJson(oldManifestPath); + } + + const modrinthChanges = await getProjectsChanges( + instanceFolder, + manifest, + oldManifest, + ); + const overridesChanges = await getOverridesChanges( + instanceFolder, + tempFolder, + ); + + return { modrinthChanges, overridesChanges }; +} + +export async function ModrinthDownloader( + addedProjects: PoolFile[], + emit: Instance["emit"], +) { + const modrinthDownloader = new DownloadPool(addedProjects, { + pQueueOptions: { + concurrency: 5, + }, + }); + + modrinthDownloader.on("completed", () => { + emit( + "progress", + "install-download-modrinth", + modrinthDownloader.done, + modrinthDownloader.total, + modrinthDownloader.doneSize, + modrinthDownloader.totalSize, + ); + }); + await modrinthDownloader.run(); +} + +export async function ProjectsDeleter( + removedProject: string[], + emit: Instance["emit"], +) { + const projectsDeleteQueue = new DeleteQueue(removedProject, { + pQueueOptions: { + concurrency: 5, + }, + }); + projectsDeleteQueue.on("completed", () => { + emit( + "progress", + "install-delete-modrinth", + projectsDeleteQueue.done, + projectsDeleteQueue.total, + projectsDeleteQueue.doneSize, + projectsDeleteQueue.totalSize, + ); + }); + await projectsDeleteQueue.run(); +} + +export async function OverridesTreeMaker( + addedDirs: string[], + emit: Instance["emit"], +) { + const overridesTreeMaker = new TreeMakerQueue(addedDirs, { + pQueueOptions: { + concurrency: 5, + }, + }); + overridesTreeMaker.on("completed", () => { + emit( + "progress", + "install-delete-overrides", + overridesTreeMaker.done, + overridesTreeMaker.total, + overridesTreeMaker.doneSize, + overridesTreeMaker.totalSize, + ); + }); + await overridesTreeMaker.run(); +} + +export async function OverridesCopier( + addedOverrides: string[], + overridesFolder: string, + instancePath: string, + emit: Instance["emit"], +) { + const overridesCopyQueue = new CopyQueue( + addedOverrides, + overridesFolder, + instancePath, + { + pQueueOptions: { + concurrency: 5, + }, + }, + ); + overridesCopyQueue.on("completed", () => { + emit( + "progress", + "install-copy-overrides", + overridesCopyQueue.done, + overridesCopyQueue.total, + overridesCopyQueue.doneSize, + overridesCopyQueue.totalSize, + ); + }); + await overridesCopyQueue.run(); +} + +export async function OverridesDeleter( + overridesChanges: { + addedOverrides: string[]; + removedOverrides: string[]; + addedDirs: string[]; + removedDirs: string[]; + }, + emit: Instance["emit"], +) { + const removedOverridesDeleteQueue = new DeleteQueue( + overridesChanges.removedOverrides, + { + pQueueOptions: { + concurrency: 5, + }, + }, + ); + removedOverridesDeleteQueue.on("completed", () => { + emit( + "progress", + "install-delete-overrides", + removedOverridesDeleteQueue.done, + removedOverridesDeleteQueue.total, + removedOverridesDeleteQueue.doneSize, + removedOverridesDeleteQueue.totalSize, + ); + }); + await removedOverridesDeleteQueue.run(); + + const removedDirsDeleteQueue = new DeleteQueue( + overridesChanges.removedOverrides, + { + pQueueOptions: { + concurrency: 5, + }, + }, + ); + removedDirsDeleteQueue.on("completed", () => { + emit( + "progress", + "install-delete-overrides", + removedDirsDeleteQueue.done, + removedDirsDeleteQueue.total, + removedDirsDeleteQueue.doneSize, + removedDirsDeleteQueue.totalSize, + ); + }); + await removedDirsDeleteQueue.run(); +} diff --git a/src/launcher/instance.ts b/src/launcher/instance.ts index 8adbfdb..751ab46 100644 --- a/src/launcher/instance.ts +++ b/src/launcher/instance.ts @@ -1,15 +1,13 @@ import { EventEmitter } from "events"; import { - BaseConfig, Config, InstanceEvents, logger, - PoolFile, Version, + FinalConfig, ModloaderConfig, } from "../utils/types"; import path from "node:path"; -import fs from "fs/promises"; import { argumentsGenerator, AssetsDownloader, @@ -27,23 +25,29 @@ import { } from "../core/modloaders"; import { ChildProcessWithoutNullStreams } from "node:child_process"; import { prepareManifest } from "../core/mergeManifests"; -import { DownloadPool } from "../utils/fetch"; +import { + generatePackChanges, + ModrinthDownloader, + OverridesCopier, + OverridesDeleter, + ProjectsDeleter, +} from "../core/modrinth"; -export function defineConfig(logger: logger, ...layers: Partial[]) { - let config: Partial = {} as Partial; - for (const layer of layers) { - config = { ...config, ...layer }; - } +export function defineConfig(logger: logger, config: Config) { if ( !config.auth || !config.paths?.root || !config.paths?.instance || - !config.version || - !config.javaExecutable + (!config.version && !config.modrinth) ) { throw new ConfigError("Invalid config provided", config, logger); } + if (config.modrinth?.pack) { + config.version = config.modrinth.version; + config.modloader = config.modrinth.modloader; + } + config.paths = { root: config.paths.root, instance: config.paths.instance, @@ -60,26 +64,22 @@ export function defineConfig(logger: logger, ...layers: Partial[]) { config.ram = { max: config.ram?.max ?? "2G", min: config.ram?.min ?? "2G" }; - if (!config.files) { - config.files = []; - } - - return config as Config; + return config as FinalConfig; } export class Instance extends EventEmitter { ready: boolean; - config: Config; + config: FinalConfig; versionLocation: string; versionManifest: Version; logger: logger; - constructor(...layers: Partial[]) { + constructor(config: Config) { super(); this.logger = (step: string, message: unknown) => { this.emit("log", step, message); }; - this.config = defineConfig(this.logger, ...layers); + this.config = defineConfig(this.logger, config); this.ready = false; this.versionManifest = {} as Version; this.versionLocation = ""; @@ -255,59 +255,39 @@ export class Instance extends EventEmitter { this.emit("progress", "install-modloader", 1, 1, 1, 1); } - if (this.config.files.length > 0) { - this.log("install-modpack-files", "Downloading modpack's files"); + if (this.config.modrinth?.pack) { try { - const files = this.config.files.map((f) => { - return { - url: f.url, - path: path.join(this.config.paths.instance, f.path), - size: f.size, - }; - }); - const pool = new DownloadPool(files, { - pQueueOptions: { concurrency: 5 }, - overwrite: false, - }); - pool.on("completed", () => { - this.emit( - "progress", - "install-modpack-files", - pool.done, - pool.total, - pool.doneSize, - pool.totalSize, - ); - }); - await pool.run(); - } catch (error) { - throw new InstallError( - "An error occured while downloading modpack's files", - "install-modpack-files", + const { modrinthChanges, overridesChanges } = await generatePackChanges( + this.config.paths.instance, + this.config.modrinth.tempFolder, this.config, - this.logger, - { cause: error }, ); - } - } - if (this.config.overridesPath) { - this.log("install-modpack-overrides", "Copying modpack's overrides"); - this.emit("progress", "install-modpack-overrides", 0, 1, 0, 1); - try { - await fs.cp(this.config.overridesPath, this.config.paths.instance, { - recursive: true, - }); + console.log(modrinthChanges, overridesChanges); + + // Delete before downloading + + await ProjectsDeleter(modrinthChanges.removedProject, (...args) => this.emit(...args)); + + await ModrinthDownloader(modrinthChanges.addedProjects, (...args) => this.emit(...args)); + + await OverridesDeleter(overridesChanges, (...args) => this.emit(...args)); + + await OverridesCopier( + overridesChanges.addedOverrides, + path.join(this.config.modrinth.tempFolder, 'pack', 'overrides'), + this.config.paths.instance, + (...args) => this.emit(...args), + ); } catch (error) { throw new InstallError( - "An error occured while copying modpack's overrides", - "install-modpack-overrides", + "An error occured while installing/updating the modpack", + "install-modpack", this.config, this.logger, { cause: error }, ); } - this.emit("progress", "install-modpack-overrides", 1, 1, 1, 1); } } diff --git a/src/launcher/modrinth.ts b/src/launcher/modrinth.ts index 4114aeb..ac030b3 100644 --- a/src/launcher/modrinth.ts +++ b/src/launcher/modrinth.ts @@ -3,45 +3,73 @@ import { downloadFile } from "../utils/fetch"; import { getTempFolder } from "../utils/temp"; import { unzipAll } from "../utils/unzip"; import { ensureDir, exists, readJson } from "../utils/fs"; -import { Modloader, ModrinthManifest, BaseConfig, PoolFile, ModrinthPack } from "../utils/types"; +import { Modloader, ModrinthManifest, ModrinthPack } from "../utils/types"; +import { ImportModpackError } from "../utils/errors"; function getModloader(manifest: ModrinthManifest): Modloader { - if (manifest.dependencies["fabric-loader"]) { - return { name: "fabric", version: manifest.dependencies["fabric-loader"] } - } else if (manifest.dependencies["forge"]) { - return { name: "forge", version: manifest.dependencies["forge"] } - } else if (manifest.dependencies["neoforge"]) { - return { name: "neoforge", version: manifest.dependencies["neoforge"] } - } else { - throw new Error("Unknown modloader") - } + if (manifest.dependencies["fabric-loader"]) { + return { name: "fabric", version: manifest.dependencies["fabric-loader"] }; + } else if (manifest.dependencies["forge"]) { + return { name: "forge", version: manifest.dependencies["forge"] }; + } else if (manifest.dependencies["neoforge"]) { + return { name: "neoforge", version: manifest.dependencies["neoforge"] }; + } else { + throw new Error("Unknown modloader"); + } } -export async function importModrinthModpack(source: string, sourceType: "url" | "file"): Promise { - const tempFolder = await getTempFolder("modrinth") +export async function importModrinthModpack( + pack: string, + updateFrom?: string, +): Promise { + try { + const tempFolder = await getTempFolder("modrinth"); - if (sourceType === "url") { - const outputFile = path.join(tempFolder, "pack.mrpack") - await downloadFile({ path: outputFile, url: source }) - source = outputFile + if (pack.startsWith("http")) { + const outputFile = path.join(tempFolder, "pack.mrpack"); + await downloadFile({ path: outputFile, url: pack }); + pack = outputFile; } - if (!await exists(source)) { - throw new Error("Couldn't find modpack file") + if (updateFrom?.startsWith("http")) { + const outputFile = path.join(tempFolder, "oldPack.mrpack"); + await downloadFile({ path: outputFile, url: updateFrom }); + updateFrom = outputFile; } - await ensureDir(path.join(tempFolder, "pack")) + if (!(await exists(pack)) || (updateFrom && !(await exists(updateFrom)))) { + throw new Error("Couldn't find modpack file"); + } - await unzipAll(source, path.join(tempFolder, "pack"), [], "overwrite") + await ensureDir(path.join(tempFolder, "pack"), true); + await unzipAll(pack, path.join(tempFolder, "pack"), [], "overwrite"); + const manifest = await readJson( + path.join(tempFolder, "pack", "modrinth.index.json"), + ); - const manifest = await readJson(path.join(tempFolder, "pack", "modrinth.index.json")) + if (updateFrom) { + await ensureDir(path.join(tempFolder, "oldPack")); + await unzipAll( + updateFrom, + path.join(tempFolder, "oldPack"), + [], + "overwrite", + ); + } return { - version: manifest.dependencies.minecraft, - modloader: getModloader(manifest), - files: manifest.files.map(f => { - return { url: f.downloads[0], path: f.path, size: f.fileSize } - }), - overridesPath: path.join(tempFolder, "pack", "overrides") - } -} \ No newline at end of file + pack, + updateFrom, + tempFolder, + version: manifest.dependencies.minecraft, + modloader: getModloader(manifest), + }; + } catch (error) { + throw new ImportModpackError( + "An error occured while importing the modpack", + "install-modloader", + { pack, updateFrom }, + { cause: error }, + ); + } +} diff --git a/src/utils/errors.ts b/src/utils/errors.ts index c2b7b17..f464cc8 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -1,20 +1,47 @@ -import { BaseConfig, Config } from "../launcher/instance" -import { logger } from "./types" +import { Config, FinalConfig } from "../utils/types"; +import { logger } from "./types"; export class ConfigError extends Error { - constructor(message: string, config: Partial, logger: logger, options?: ErrorOptions) { - super(message, options) - logger("error", "Provided config was:") - console.log(config) + constructor( + message: string, + config: Config, + logger: logger, + options?: ErrorOptions, + ) { + super(message, options); + logger("error", "Provided config was:"); + console.log(config); } } export class InstallError extends Error { - constructor(message: string, step: string, config: Config, logger: logger, options?: ErrorOptions) { - super(`[${step}] ${message}`, options) - logger("error", "Provided config was:") - console.log(config) + constructor( + message: string, + step: string, + config: FinalConfig, + logger: logger, + options?: ErrorOptions, + ) { + super(`[${step}] ${message}`, options); + logger("error", "Provided config was:"); + console.log(config); + console.log('Original error is:') + console.error(options?.cause) } } -export class LaunchError extends InstallError { } \ No newline at end of file +export class LaunchError extends InstallError {} + +export class ImportModpackError extends Error { + constructor( + message: string, + step: string, + modrinth: { pack: string; updateFrom?: string }, + options?: ErrorOptions, + ) { + super(`[${step}] ${message}`, options); + console.log("error", "Modpack to install was:"); + console.log(`Modpack: ${modrinth.pack}`); + if (modrinth.updateFrom) console.log(`Update from: ${modrinth.updateFrom}`); + } +} diff --git a/src/utils/fetch.ts b/src/utils/fetch.ts index 7626ae4..52669f4 100644 --- a/src/utils/fetch.ts +++ b/src/utils/fetch.ts @@ -46,9 +46,9 @@ export class DownloadPool extends PQueue { this.total = this.elements.length; this.cleanup = options.cleanup; if (options.overwrite) { - this.overwrite = true + this.overwrite = true; } else { - this.overwrite = false + this.overwrite = false; } } @@ -61,12 +61,13 @@ export class DownloadPool extends PQueue { // update status here to ensure that it is run before "completed" events listeners this.done++; this.doneSize += element.size!; - }); + }).catch(() => {}); } - await this.onIdle(); + await Promise.race([this.onError(), this.onIdle()]); + if (this.cleanup) { await this.cleanup(); } } -} \ No newline at end of file +} diff --git a/src/utils/fs.ts b/src/utils/fs.ts index 1625934..ba97a9c 100644 --- a/src/utils/fs.ts +++ b/src/utils/fs.ts @@ -1,4 +1,7 @@ import fs from "fs/promises"; +import PQueue from "p-queue"; +import { type Options as PQueueOptions } from "p-queue"; +import path from "path"; export async function exists(path: string) { try { @@ -19,3 +22,135 @@ export async function ensureDir(path: string, recursive = false) { export async function readJson(destination: string): Promise { return JSON.parse(await fs.readFile(destination, { encoding: "utf-8" })); } + +export class DeleteQueue extends PQueue { + done = 0; + total = 0; + doneSize = 0; + totalSize = 0; + elements: string[]; + cleanup?: () => Promise; + + constructor( + elements: string[], + options: { + //@ts-expect-error can't figure out what to put as arguments of PQueueOptions + pQueueOptions: PQueueOptions; + cleanup?: () => Promise; + } = { + pQueueOptions: {}, + }, + ) { + //@ts-expect-error can't figure out what to put as arguments of PQueueOptions + super(options.pQueueOptions); + this.elements = elements; + this.total = this.elements.length; + this.cleanup = options.cleanup; + } + + async run() { + for (const element of this.elements) { + this.add(async () => { + await fs.rm(element, { recursive: true, force: true }); + this.done++; + }).catch(() => {}); + } + + await Promise.race([this.onError(), this.onIdle()]); + if (this.cleanup) { + await this.cleanup(); + } + } +} + +export class CopyQueue extends PQueue { + done = 0; + total = 0; + doneSize = 0; + totalSize = 0; + files: string[]; + source: string; + destination: string; + cleanup?: () => Promise; + + constructor( + files: string[], + source: string, + destination: string, + options: { + //@ts-expect-error can't figure out what to put as arguments of PQueueOptions + pQueueOptions: PQueueOptions; + cleanup?: () => Promise; + } = { + pQueueOptions: {}, + }, + ) { + //@ts-expect-error can't figure out what to put as arguments of PQueueOptions + super(options.pQueueOptions); + this.files = files; + this.source = source; + this.destination = destination; + this.total = this.files.length; + this.cleanup = options.cleanup; + } + + async run() { + for (const file of this.files) { + this.add(async () => { + await fs.cp( + path.join(this.source, file), + path.join(this.destination, file), + { + force: true, + }, + ); + this.done++; + }).catch(() => {}); + } + + await Promise.race([this.onError(), this.onIdle()]); + if (this.cleanup) { + await this.cleanup(); + } + } +} + +export class TreeMakerQueue extends PQueue { + done = 0; + total = 0; + doneSize = 0; + totalSize = 0; + addedDirs: string[]; + cleanup?: () => Promise; + + constructor( + addedDirs: string[], + options: { + //@ts-expect-error can't figure out what to put as arguments of PQueueOptions + pQueueOptions: PQueueOptions; + cleanup?: () => Promise; + } = { + pQueueOptions: {}, + }, + ) { + //@ts-expect-error can't figure out what to put as arguments of PQueueOptions + super(options.pQueueOptions); + this.addedDirs = addedDirs; + this.total = this.addedDirs.length; + this.cleanup = options.cleanup; + } + + async run() { + for (const dir of this.addedDirs) { + this.add(async () => { + await ensureDir(dir, true); + this.done++; + }).catch(() => {}); + } + + await Promise.race([this.onError(), this.onIdle()]); + if (this.cleanup) { + await this.cleanup(); + } + } +} diff --git a/src/utils/types.ts b/src/utils/types.ts index a62336d..b21c24b 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -196,6 +196,12 @@ export type Paths = { libraries?: string; }; +export interface FilledPaths extends Paths { + versions: string; + assets: string; + libraries: string; +} + export type LaunchErrorConfig = { version: string; paths: Paths; @@ -244,11 +250,21 @@ export type ProcessArgs = { java?: string; }; +export interface FilledProcessArgs { + game: string; + java: string; +} + export type ProcessRam = { max?: string; min?: string; }; +export interface FilledProcessRam extends ProcessRam { + max: string; + min: string; +} + export type logger = (step: string, message: unknown) => void; export type FabricInstallerMeta = { @@ -280,32 +296,32 @@ export type ModrinthManifest = { }; }; -export interface BaseConfig { +export type ModrinthPack = { + pack: string; + updateFrom?: string; + tempFolder: string; version: string; + modloader: Modloader; +}; + +export type Config = { + version?: string; auth: Auth; paths: Paths; javaExecutable: string; modloader?: Modloader; args?: ProcessArgs; - ram?: { max?: string; min?: string }; - files?: PoolFile[]; - overridesPath?: string; -} + ram?: ProcessRam; + modrinth?: ModrinthPack; +}; -export interface Config extends BaseConfig { - paths: Required; - args: Required; - ram: Required; - files: PoolFile[]; -} - -export interface ModloaderConfig extends Config { - modloader: Modloader; -} - -export interface ModrinthPack { +export interface FinalConfig extends Config { version: string; - modloader: Modloader; - files: PoolFile[]; - overridesPath: string; + paths: FilledPaths; + args: FilledProcessArgs; + ram: FilledProcessRam; +} + +export interface ModloaderConfig extends FinalConfig { + modloader: Modloader; } diff --git a/tests/modrinth-update.test.ts b/tests/modrinth-update.test.ts new file mode 100644 index 0000000..61562c4 --- /dev/null +++ b/tests/modrinth-update.test.ts @@ -0,0 +1,123 @@ +import { test } from "vitest"; +import { + Instance, + offlineAuth, + RuntimeManager, + getJavaComponent, + importModrinthModpack, +} from "../dist/index.js"; +import path from "path"; +import fs from "fs/promises"; + +const gameRoot = path.join(import.meta.dirname, "temp"); +await fs.rm(path.join(gameRoot, "instances", "modrinth_update"), { + recursive: true, + force: true, +}); // remove instance dir each start to ensure that first launch works (else files from update will be used to start original) + +test( + "update modrinth modpack", + { timeout: 0, tags: ["modrinth"] }, + async () => { + { + const modpack = await importModrinthModpack( + "https://cdn.modrinth.com/data/1KVo5zza/versions/JnvfUoIW/Fabulously.Optimized-v13.2.1.mrpack", + ); + + const javaManager = new RuntimeManager(path.join(gameRoot, "java")); + javaManager.on("progress", console.log); + const java = await javaManager.use( + await getJavaComponent(modpack.version), + ); + + const instance = new Instance({ + auth: offlineAuth("player"), + paths: { + root: gameRoot, + instance: path.join(gameRoot, "instances", "modrinth_update"), + }, + javaExecutable: java, + ram: { max: "4G", min: "4G" }, + modrinth: modpack, + }); + + instance.on("progress", console.log); + instance.on("log", (step, msg) => { + console.log(`[${step}] ${msg}`); + }); + + await instance.install(); + const p = await instance.launch(); + + p.stdout.on("data", (d: Buffer) => { + console.log(d.toString()); + }); + + p.stderr.on("data", (d: Buffer) => { + console.log(d.toString()); + }); + + await new Promise((res) => { + p.on("error", (d: Buffer) => { + console.log(d.toString()); + res(); + }); + p.on("close", () => { + console.log("closed"); + res(); + }); + }); + } + + /* + to test that everything works i try to fully replace fabulously optimized by a completly different modpack + but it is intended to be used only to update from one modpack to its update + */ + const modpack = await importModrinthModpack( + "https://cdn.modrinth.com/data/l9m9tuPN/versions/M8j2mfGj/Zombie%20Invade%20100%20Days%202.3.mrpack", + "https://cdn.modrinth.com/data/1KVo5zza/versions/JnvfUoIW/Fabulously.Optimized-v13.2.1.mrpack", + ); + const javaManager = new RuntimeManager(path.join(gameRoot, "java")); + javaManager.on("progress", console.log); + const java = await javaManager.use(await getJavaComponent(modpack.version)); + + const instance = new Instance({ + auth: offlineAuth("player"), + paths: { + root: gameRoot, + instance: path.join(gameRoot, "instances", "modrinth_update"), + }, + javaExecutable: java, + ram: { max: "4G", min: "4G" }, + modrinth: modpack, + }); + + instance.on("progress", console.log); + instance.on("log", (step, msg) => { + console.log(`[${step}] ${msg}`); + }); + + await instance.install(); + + const p = await instance.launch(); + + p.stdout.on("data", (d: Buffer) => { + console.log(d.toString()); + }); + + p.stderr.on("data", (d: Buffer) => { + console.log(d.toString()); + }); + + await new Promise((res) => { + p.on("error", (d: Buffer) => { + console.log(d.toString()); + res(); + }); + p.on("close", () => { + console.log("closed"); + res(); + }); + }); + }, +);