feat: rewrite entirely the modrinth installer and allow to update already installed modpack to a newer version

This commit is contained in:
HerozDotExe 2026-06-26 16:01:05 +02:00
parent 6bcde4cd8c
commit 82af1e947f
12 changed files with 1129 additions and 184 deletions

View file

@ -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",

318
pnpm-lock.yaml generated
View file

@ -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

View file

@ -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[] = [];

View file

@ -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<Version>(
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
return versionManifest;
}

379
src/core/modrinth.ts Normal file
View file

@ -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<string, ModrinthManifestFile>();
const oldFiles = new Map<string, ModrinthManifestFile>();
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<PoolFile>((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<ModrinthManifest>(manifestPath);
let oldManifest: ModrinthManifest | undefined;
if (config.modrinth?.updateFrom) {
const oldManifestPath = path.join(
tempFolder,
"oldPack",
"modrinth.index.json",
);
oldManifest = await readJson<ModrinthManifest>(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();
}

View file

@ -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<BaseConfig>[]) {
let config: Partial<BaseConfig> = {} as Partial<BaseConfig>;
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<BaseConfig>[]) {
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<InstanceEvents> {
ready: boolean;
config: Config;
config: FinalConfig;
versionLocation: string;
versionManifest: Version;
logger: logger;
constructor(...layers: Partial<BaseConfig>[]) {
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<InstanceEvents> {
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<PoolFile>((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);
}
}

View file

@ -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<ModrinthPack> {
const tempFolder = await getTempFolder("modrinth")
export async function importModrinthModpack(
pack: string,
updateFrom?: string,
): Promise<ModrinthPack> {
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<ModrinthManifest>(
path.join(tempFolder, "pack", "modrinth.index.json"),
);
const manifest = await readJson<ModrinthManifest>(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<PoolFile>(f => {
return { url: f.downloads[0], path: f.path, size: f.fileSize }
}),
overridesPath: path.join(tempFolder, "pack", "overrides")
}
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 },
);
}
}

View file

@ -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<BaseConfig>, 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 { }
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}`);
}
}

View file

@ -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,10 +61,11 @@ 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();
}

View file

@ -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<T>(destination: string): Promise<T> {
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<void>;
constructor(
elements: string[],
options: {
//@ts-expect-error can't figure out what to put as arguments of PQueueOptions
pQueueOptions: PQueueOptions<null, null>;
cleanup?: () => Promise<void>;
} = {
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<void>;
constructor(
files: string[],
source: string,
destination: string,
options: {
//@ts-expect-error can't figure out what to put as arguments of PQueueOptions
pQueueOptions: PQueueOptions<null, null>;
cleanup?: () => Promise<void>;
} = {
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<void>;
constructor(
addedDirs: string[],
options: {
//@ts-expect-error can't figure out what to put as arguments of PQueueOptions
pQueueOptions: PQueueOptions<null, null>;
cleanup?: () => Promise<void>;
} = {
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();
}
}
}

View file

@ -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<Paths>;
args: Required<ProcessArgs>;
ram: Required<ProcessRam>;
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;
}

View file

@ -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<void>((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<void>((res) => {
p.on("error", (d: Buffer) => {
console.log(d.toString());
res();
});
p.on("close", () => {
console.log("closed");
res();
});
});
},
);