feat: add a function to download natives and its test

This commit is contained in:
HerozDotExe 2025-10-05 21:50:33 +02:00
parent dd548c5f26
commit b6b5da3981
13 changed files with 215 additions and 43 deletions

View file

@ -21,17 +21,16 @@
"packageManager": "pnpm@10.17.1",
"devDependencies": {
"@eslint/js": "^9.36.0",
"@types/adm-zip": "^0.5.7",
"@types/node": "^24.5.2",
"adm-zip": "^0.5.16",
"eslint": "^9.36.0",
"eslint-config-prettier": "^10.1.8",
"globals": "^16.4.0",
"p-queue": "^8.1.1",
"prettier": "3.6.2",
"tsup": "^8.5.0",
"typescript-eslint": "^8.44.1",
"vitest": "^3.2.4"
},
"dependencies": {
"p-limit": "^7.1.1",
"p-queue": "^8.1.1"
}
}

43
pnpm-lock.yaml generated
View file

@ -7,20 +7,19 @@ settings:
importers:
.:
dependencies:
p-limit:
specifier: ^7.1.1
version: 7.1.1
p-queue:
specifier: ^8.1.1
version: 8.1.1
devDependencies:
'@eslint/js':
specifier: ^9.36.0
version: 9.36.0
'@types/adm-zip':
specifier: ^0.5.7
version: 0.5.7
'@types/node':
specifier: ^24.5.2
version: 24.5.2
adm-zip:
specifier: ^0.5.16
version: 0.5.16
eslint:
specifier: ^9.36.0
version: 9.36.0
@ -30,6 +29,9 @@ importers:
globals:
specifier: ^16.4.0
version: 16.4.0
p-queue:
specifier: ^8.1.1
version: 8.1.1
prettier:
specifier: 3.6.2
version: 3.6.2
@ -398,6 +400,9 @@ packages:
cpu: [x64]
os: [win32]
'@types/adm-zip@0.5.7':
resolution: {integrity: sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw==}
'@types/chai@5.2.2':
resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==}
@ -511,6 +516,10 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
adm-zip@0.5.16:
resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==}
engines: {node: '>=12.0'}
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@ -929,10 +938,6 @@ packages:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
p-limit@7.1.1:
resolution: {integrity: sha512-i8PyM2JnsNChVSYWLr2BAjNoLi0BAYC+wecOnZnVV+YSNJkzP7cWmvI34dk0WArWfH9KwBHNoZI3P3MppImlIA==}
engines: {node: '>=20'}
p-locate@5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
@ -1315,10 +1320,6 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
yocto-queue@1.2.1:
resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==}
engines: {node: '>=12.20'}
snapshots:
'@esbuild/aix-ppc64@0.25.10':
@ -1558,6 +1559,10 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.52.2':
optional: true
'@types/adm-zip@0.5.7':
dependencies:
'@types/node': 24.5.2
'@types/chai@5.2.2':
dependencies:
'@types/deep-eql': 4.0.2
@ -1713,6 +1718,8 @@ snapshots:
acorn@8.15.0: {}
adm-zip@0.5.16: {}
ajv@6.12.6:
dependencies:
fast-deep-equal: 3.1.3
@ -2131,10 +2138,6 @@ snapshots:
dependencies:
yocto-queue: 0.1.0
p-limit@7.1.1:
dependencies:
yocto-queue: 1.2.1
p-locate@5.0.0:
dependencies:
p-limit: 3.1.0
@ -2499,5 +2502,3 @@ snapshots:
strip-ansi: 7.1.2
yocto-queue@0.1.0: {}
yocto-queue@1.2.1: {}

View file

@ -1,2 +1,3 @@
export * as java from "./java"
export * as version from "./version"
export * as java from "./java";
export * as version from "./version";
export * as natives from "./natives";

61
src/core/natives.ts Normal file
View file

@ -0,0 +1,61 @@
import { Library, Native, PoolFile, Version } from "../utils/types";
import path from "path";
import { ensureDir } from "../utils/fs";
import { parseRule } from "../utils/rules";
import { os } from "../utils/systemInfo";
import { DownloadPool } from "../utils/fetch";
import { getTempFolder } from "../utils/temp";
import { unzipAll } from "../utils/unzip";
export async function download(destination: string, versionManifest: Version) {
const nativesPath = path.join(destination);
await ensureDir(nativesPath);
const tempNativesPath = await getTempFolder("natives");
// Download archives to a temp folder
const files = versionManifest.libraries.map<PoolFile>((library: Library) => {
if (library.downloads && library.downloads.classifiers) {
if (parseRule(library)) return null;
const native: Native =
os() === "osx"
? library.downloads.classifiers["natives-osx"] ||
library.downloads.classifiers["natives-macos"]
: library.downloads.classifiers[`natives-${os()}`];
const name = native.path.split("/").pop();
return { url: native.url, path: path.join(tempNativesPath, name) };
}
});
const dPool = new DownloadPool(files, 5);
await dPool.run();
// Extract every archives into the natives folder
for (const key in files) {
if (Object.prototype.hasOwnProperty.call(files, key)) {
const file = files[key];
if (file) unzipAll(file.path, nativesPath);
}
}
// for (const key in versionManifest.libraries) {
// if (Object.prototype.hasOwnProperty.call(versionManifest.libraries, key)) {
// const library = versionManifest.libraries[key];
// if (library.downloads && library.downloads.classifiers) {
// if (parseRule(library)) continue;
// const native: Native =
// this.getOS() === "osx"
// ? library.downloads.classifiers["natives-osx"] ||
// library.downloads.classifiers["natives-macos"]
// : library.downloads.classifiers[`natives-${os()}`];
// const name = native.path.split("/").pop();
// }
// }
// }
}

View file

@ -12,6 +12,7 @@ export async function downloadFile(
signal: AbortSignal,
) {
try {
if (!file) return;
const res = await fetch(file.url, { signal });
if (!res.ok)

17
src/utils/fs.ts Normal file
View file

@ -0,0 +1,17 @@
import fs from "fs/promises";
export async function exists(path: string) {
try {
await fs.access(path, fs.constants.F_OK);
return true;
} catch {
return false;
}
}
export async function ensureDir(path: string, recursive = false) {
if (!(await exists(path))) {
await fs.mkdir(path, { recursive });
return true;
} else return false;
}

23
src/utils/rules.ts Normal file
View file

@ -0,0 +1,23 @@
import { os } from "./systemInfo";
import { Version } from "./types";
// Copied from https://github.com/Pierce01/MinecraftLauncher-core/blob/f4ce947658e82218011d92c36d4d8a1b8c0c2429/components/handler.js#L237
export function parseRule(library: Version["libraries"][number]): boolean {
if (library.rules) {
if (library.rules.length > 1) {
if (
library.rules[0].action === "allow" &&
library.rules[1].action === "disallow" &&
library.rules[1].os.name === "osx"
) {
return os() === "osx";
}
return true;
} else {
if (library.rules[0].action === "allow" && library.rules[0].os)
return library.rules[0].os.name !== os();
}
} else {
return false;
}
}

10
src/utils/systemInfo.ts Normal file
View file

@ -0,0 +1,10 @@
export function os() {
switch (process.platform) {
case "win32":
return "windows";
case "darwin":
return "osx";
default:
return "linux";
}
}

15
src/utils/temp.ts Normal file
View file

@ -0,0 +1,15 @@
import { tmpdir } from "os";
import path from "path";
import { ensureDir } from "./fs";
import fs from "fs/promises";
/**
* Returns an empty temporary folder.
* The folder gets wiped before every call.
*/
export async function getTempFolder(name: string) {
const tempPath = path.join(tmpdir(), "nlk", name);
await fs.rm(tempPath, { force: true, recursive: true });
await ensureDir(tempPath, true);
return tempPath;
}

View file

@ -74,15 +74,45 @@ export type Versions = {
}[];
};
export type Native = {
path: string;
sha1: string;
size: number;
url: string;
};
type NativeOS = "natives-linux" | "natives-windows" | "natives-macos";
type JVMRule = {
action: string;
os: { name?: string; version?: string; arch?: string };
};
type LibraryRule = { action: string; os?: { name: string } };
export type Library = {
downloads: {
artifacts: {
path: string;
sha1: string;
size: number;
url: string;
};
classifiers?: {
[key in NativeOS]: Native;
};
};
name: string;
rules?: LibraryRule[];
natives?: { [key: string]: string };
};
export type Version = {
arguments: {
game: string[];
jvm:
| {
rules: {
action: string;
os: { name?: string; version?: string; arch?: string };
}[];
rules: JVMRule[];
value: string[] | string;
}
| string[];
@ -107,18 +137,7 @@ export type Version = {
component: RuntimeComponent;
majorVersion: number;
};
libraries: {
downloads: {
artifacts: {
path: string;
sha1: string;
size: number;
url: string;
};
name: string;
rules: { action: string; os: { name: string } }[];
};
}[];
libraries: Library[];
logging: {
client: {
argument: string;

6
src/utils/unzip.ts Normal file
View file

@ -0,0 +1,6 @@
import AdmZip from "adm-zip";
export function unzipAll(from: string, to: string) {
const zip = new AdmZip(from);
zip.extractAllTo(to, true);
}

18
tests/natives.test.ts Normal file
View file

@ -0,0 +1,18 @@
import { expect, test } from "vitest";
import * as nlk from "../dist/index.js";
import path from "path";
import fs from "fs/promises";
const nativesPath = path.join(import.meta.dirname, "temp/natives");
console.log(nativesPath)
await fs.rm(nativesPath, { recursive: true, force: true });
await fs.mkdir(nativesPath, { recursive: true });
test(
"download natives for 1.15 correctly",
{ timeout: 10000 },
async () => {
const versionManifest = await nlk.core.version.getVersionManifest("1.15");
await nlk.core.natives.download(nativesPath, versionManifest);
},
);

View file

@ -5,4 +5,5 @@ export default defineConfig({
target: "node22",
format: ["cjs", "esm"],
dts: true,
sourcemap: true,
});