Add initial minecraft-java-manager scaffold

Initial project scaffold for minecraft-java-manager. Adds package.json and package-lock.json, README update, index.js entrypoint and test.js. Introduces src modules: checker (detects Java version), installer (downloads JRE components with redirects, SHA1 validation, progress/events and concurrency), web (fetches launcher/runtime manifests and component data), systeminfo (platform key and install path resolution) and config (endpoints). Declares dependencies adm-zip and node-downloader-helper.
This commit is contained in:
2026-09-06 03:58:25 +02:00
parent 8750661f49
commit e9bc3807ae
10 changed files with 516 additions and 1 deletions
+2
View File
@@ -1,3 +1,5 @@
# minecraft-java-manager # minecraft-java-manager
Node module to check if java is installed and install it Node module to check if java is installed and install it
There is two function, read index.js
+24
View File
@@ -0,0 +1,24 @@
const installer = require("./src/installer")
const checker = require("./src/checker")
const sysinfo = require("./src/systeminfo")
const web = require("./src/web")
async function isJavaInstalledAndCompatibleFor(mcVersion, customExec = "java") {
const mcVersionManifestUrl = await web.getVersionManifest(mcVersion)
const requiredJRE = await web.getRequiredJREFor(mcVersionManifestUrl)
const checkJRECommand = checker.getJavaVersionOutput(customExec)
const isJavaCompatible = checker.isJavaCompatible(checkJRECommand, requiredJRE.majorVersion)
return isJavaCompatible
}
async function installJava(component, majorVersion, customInstallPath = null) {
const installationPath = await sysinfo.determineInstallationPath(component, majorVersion, customInstallPath)
const requiredComponentDetails = await web.getJREComponentDetails(component, sysinfo.getPlatformKey())
const requiredComponentManifest = await web.getJREComponentManifest(requiredComponentDetails[0].manifest.url)
return await installer.downloadJavaComponent(requiredComponentManifest, installationPath)
}
module.exports = {
isJavaInstalledAndCompatibleFor,
installJava
}
+38
View File
@@ -0,0 +1,38 @@
{
"name": "minecraft-java-manager",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "minecraft-java-manager",
"version": "0.0.1",
"license": "MIT",
"dependencies": {
"adm-zip": "^0.6.0",
"node-downloader-helper": "^2.1.11"
}
},
"node_modules/adm-zip": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.0.tgz",
"integrity": "sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==",
"license": "MIT",
"engines": {
"node": ">=14.0"
}
},
"node_modules/node-downloader-helper": {
"version": "2.1.11",
"resolved": "https://registry.npmjs.org/node-downloader-helper/-/node-downloader-helper-2.1.11.tgz",
"integrity": "sha512-882fH2C9AWdiPCwz/2beq5t8FGMZK9Dx8TJUOIxzMCbvG7XUKM5BuJwN5f0NKo4SCQK6jR4p2TPm54mYGdGchQ==",
"license": "MIT",
"bin": {
"ndh": "bin/ndh"
},
"engines": {
"node": ">=14.18"
}
}
}
}
+24
View File
@@ -0,0 +1,24 @@
{
"name": "minecraft-java-manager",
"version": "0.0.1",
"description": "Node module to check if java is installed and install it",
"repository": {
"type": "git",
"url": "https://gitea.azures.fr/azures04/minecraft-java-manager"
},
"license": "MIT",
"author": {
"name": "azures04",
"url": "https://gitea.azures.fr",
"email": "azures04@azures.fr"
},
"type": "commonjs",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"adm-zip": "^0.6.0",
"node-downloader-helper": "^2.1.11"
}
}
+37
View File
@@ -0,0 +1,37 @@
const childProcess = require("node:child_process")
function getJavaVersionOutput(defaultCommand = "java") {
try {
const command = childProcess.spawnSync(defaultCommand, ["-version"], {
encoding: "utf-8",
windowsHide: true,
})
if (command.error) {
return null
}
return command.stderr || command.stdout || ""
} catch {
return null
}
}
function parseJavaOutput(output) {
if (!output || typeof output !== "string") {
return null
}
const match = output.match(/version\s+"(?:1\.)?(\d+)/i)
return match ? parseInt(match[1], 10) : null
}
function isJavaCompatible(output, requiredMajor) {
return parseJavaOutput(output) === requiredMajor
}
module.exports = {
getJavaVersionOutput,
isJavaCompatible,
parseJavaOutput,
}
+31
View File
@@ -0,0 +1,31 @@
const endpoints = {
allRuntimes: process.env.ALL_RUNTIMES || "https://launchermeta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json",
launcherManifest: process.env.LAUNCHER_MANIFEST || "https://launchermeta.mojang.com/mc/game/version_manifest_v2.json",
}
function setEndpoint(key, value) {
return endpoints[key] = value
}
function getEndpoint(key) {
return endpoints[key]
}
module.exports = {
allRuntimes: {
set: (value) => {
return setEndpoint("allRuntimes", value)
},
get: () => {
return getEndpoint("allRuntimes")
}
},
launcherManifest: {
set: (value) => {
return setEndpoint("launcherManifest", value)
},
get: () => {
return getEndpoint("launcherManifest")
}
}
}
+247
View File
@@ -0,0 +1,247 @@
const https = require("https")
const fs = require("fs")
const path = require("path")
const crypto = require("crypto")
const { EventEmitter } = require("events")
const web = require("./web")
const sysinfo = require("./systeminfo")
function extractDownloadableFiles(manifest) {
const filePaths = Object.keys(manifest.files)
const entries = []
for (const filePath of filePaths) {
const fileEntry = manifest.files[filePath]
if (fileEntry.type !== "file") {
continue
}
const downloadInfo = fileEntry.downloads.raw || fileEntry.downloads.lzma
if (!downloadInfo) {
continue
}
entries.push({
filePath: filePath,
url: downloadInfo.url,
sha1: downloadInfo.sha1,
size: downloadInfo.size,
executable: fileEntry.executable || false
})
}
return entries
}
function requestWithRedirect(url, onResponse, maxRedirects = 5) {
return new Promise((resolve, reject) => {
function makeRequest(currentUrl, redirectsLeft) {
https.get(currentUrl, (response) => {
const statusCode = response.statusCode
if (statusCode >= 300 && statusCode < 400 && response.headers.location) {
response.resume()
if (redirectsLeft <= 0) {
reject(new Error("Too many http redirection"))
return
}
makeRequest(response.headers.location, redirectsLeft - 1)
return
}
if (statusCode !== 200) {
response.resume()
reject(new Error(`HTTP Failed: received status code ${statusCode} for ${currentUrl}`))
return
}
try {
onResponse(response)
resolve()
} catch (err) {
reject(err)
}
}).on("error", (err) => {
reject(err)
})
}
makeRequest(url, maxRedirects)
})
}
async function downloadSingleFile(fileEntry, outputRoot, onProgress) {
const outputPath = path.join(outputRoot, fileEntry.filePath)
await fs.promises.mkdir(path.dirname(outputPath), {
recursive: true
})
const hasher = crypto.createHash("sha1")
const writeStream = fs.createWriteStream(outputPath)
let downloadedBytes = 0
try {
await requestWithRedirect(fileEntry.url, (responseStream) => {
responseStream.on("data", (chunk) => {
downloadedBytes += chunk.length
hasher.update(chunk)
if (onProgress) {
onProgress({
filePath: fileEntry.filePath,
bytesDownloaded: downloadedBytes,
totalBytes: fileEntry.size
})
}
})
responseStream.pipe(writeStream)
})
await new Promise((resolve, reject) => {
writeStream.on("finish", resolve)
writeStream.on("error", reject)
})
const computedSha1 = hasher.digest("hex")
if (computedSha1 !== fileEntry.sha1) {
await fs.promises.unlink(outputPath).catch(() => {})
throw new Error(
`Hash SHA1 invalide pour ${fileEntry.filePath}. ` +
`Attendu: ${fileEntry.sha1}, Obtenu: ${computedSha1}`
)
}
if (fileEntry.executable && process.platform !== "win32") {
await fs.promises.chmod(outputPath, 0o755).catch(() => {})
}
return {
filePath: fileEntry.filePath,
outputPath: outputPath,
sha1: computedSha1,
totalBytes: downloadedBytes
}
} catch (err) {
if (!writeStream.destroyed) {
writeStream.destroy()
}
throw err
}
}
async function downloadJavaComponent(manifest, outputRoot, options) {
const emitter = new EventEmitter()
const concurrency = (options && options.concurrency) || 5
try {
const files = extractDownloadableFiles(manifest)
const totalFiles = files.length
const totalBytesExpected = files.reduce((sum, f) => { return sum + (f.size || 0) }, 0)
emitter.emit("start", {
totalFiles: totalFiles,
totalBytes: totalBytesExpected,
outputRoot: outputRoot
})
let completedFiles = 0
let failedFiles = 0
let totalBytesDownloaded = 0
const errors = []
const perFileProgress = {}
function reportGlobalProgress() {
const currentTotal = Object.keys(perFileProgress).reduce((sum, key) => {
return sum + perFileProgress[key]
}, 0)
emitter.emit("progress", {
completedFiles: completedFiles,
totalFiles: totalFiles,
bytesDownloaded: currentTotal,
totalBytes: totalBytesExpected,
percent: totalBytesExpected > 0 ?
Math.round((currentTotal / totalBytesExpected) * 100) :
null
})
}
let cursor = 0
async function worker() {
while (cursor < files.length) {
const currentIndex = cursor
cursor += 1
const fileEntry = files[currentIndex]
try {
const result = await downloadSingleFile(fileEntry, outputRoot, (progressInfo) => {
perFileProgress[progressInfo.filePath] = progressInfo.bytesDownloaded
reportGlobalProgress()
})
completedFiles += 1
totalBytesDownloaded += result.totalBytes
perFileProgress[fileEntry.filePath] = result.totalBytes
emitter.emit("fileComplete", result)
reportGlobalProgress()
} catch (err) {
failedFiles += 1
errors.push({
filePath: fileEntry.filePath,
message: err.message
})
emitter.emit("fileError", {
filePath: fileEntry.filePath,
message: err.message,
error: err
})
}
}
}
const workerCount = Math.min(concurrency, files.length)
const workers = []
for (let i = 0; i < workerCount; i += 1) {
workers.push(worker())
}
await Promise.all(workers)
emitter.emit("complete", {
totalFiles: totalFiles,
completedFiles: completedFiles,
failedFiles: failedFiles,
totalBytes: totalBytesDownloaded,
errors: errors
})
} catch (err) {
emitter.emit("error", {
message: err.message,
error: err
})
}
return emitter
}
module.exports = {
downloadJavaComponent
}
+65
View File
@@ -0,0 +1,65 @@
const fs = require("node:fs")
const path = require("node:path")
function getPlatformKey() {
const platform = process.platform
const arch = process.arch
let candidate = null
if (platform === "win32") {
if (arch === "arm64") {
candidate = "windows-arm64"
}
else if (arch === "x64") {
candidate = "windows-x64"
}
else if (arch === "ia32") {
candidate = "windows-x86"
}
} else if (platform === "darwin") {
if (arch === "arm64") {
candidate = "mac-os-arm64"
} else {
candidate = "mac-os"
}
} else if (platform === "linux") {
if (arch === "ia32") {
candidate = "linux-i386"
} else {
candidate = "linux"
}
}
if (candidate) {
return candidate
}
return targets["linux"] ?? null
}
async function determineInstallationPath(component, majorVersion, customInstallPath = null) {
if (typeof customInstallPath == "string") {
try {
await fs.promises.access(customInstallPath)
return customInstallPath
} catch (error) {
await fs.promises.mkdir(customInstallPath, { recursive: true })
return customInstallPath
}
}
switch (process.platform) {
case "win32":
return path.join("Program files", "Java", component, majorVersion)
case "linux":
return path.join("opt", "java", component, majorVersion)
case "macos":
return path.join("Library", "Java", "JavaVirtualMachines", `${component}-${majorVersion}`)
default:
throw new Error("Unsupported OS")
}
}
module.exports = {
getPlatformKey,
determineInstallationPath
}
+40
View File
@@ -0,0 +1,40 @@
const config = require("./config")
async function getVersionManifest(version) {
const mainifestRequest = await fetch(config.launcherManifest.get())
const manifest = await mainifestRequest.json()
const component = manifest.versions.find($version => $version.id == version)
if (!typeof component == "object") {
throw new Error("Unknown version")
}
return component.url
}
async function getRequiredJREFor(versionUrl) {
const mainifestRequest = await fetch(versionUrl)
const manifest = await mainifestRequest.json()
return manifest.javaVersion
}
async function getJREComponentDetails(componentRequested, os) {
const mainifestRequest = await fetch(config.allRuntimes.get())
const manifest = await mainifestRequest.json()
const component = manifest[os][componentRequested]
if (typeof component != "object") {
throw new Error("No JRE Component found")
}
return component
}
async function getJREComponentManifest(url) {
const mainifestRequest = await fetch(url)
const manifest = await mainifestRequest.json()
return manifest
}
module.exports = {
getVersionManifest,
getRequiredJREFor,
getJREComponentDetails,
getJREComponentManifest
}
+7
View File
@@ -0,0 +1,7 @@
const lib = require("./index")
async function main() {
await lib.installJava("jre-legacy", 8, "./java")
}
main()