start over
This commit is contained in:
Generated
+60
-25
@@ -9,9 +9,10 @@
|
||||
"version": "0.2.0",
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@modrinth-ts/lib": "^2.1.0",
|
||||
"@sugoidogo/js-util": "^0.3.1",
|
||||
"@sugoidogo/node-file-system-adapter": "^1.0.3",
|
||||
"@xmcl/curseforge": "^2.2.0",
|
||||
"@xmcl/modrinth": "^2.5.0",
|
||||
"commander": "^14.0.3",
|
||||
"confbox": "^0.2.4",
|
||||
"curseforge-v2": "^1.5.0",
|
||||
@@ -22,9 +23,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sugoidogo/importable-types-web": "^1.0.0",
|
||||
"@sugoidogo/node-web-globals": "^2.0.0",
|
||||
"@types/md5": "^2.3.6",
|
||||
"@types/node": "^25.2.2",
|
||||
"@types/web": "^0.0.329",
|
||||
"deno": "^2.6.8",
|
||||
"typedoc": "^0.28.16",
|
||||
"typescript": "^5.9.3",
|
||||
@@ -1224,18 +1225,6 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@modrinth-ts/lib": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@modrinth-ts/lib/-/lib-2.1.0.tgz",
|
||||
"integrity": "sha512-W+HGQgQSmK/bmQO2hjEQROwMRZUSH+uigke4Zat1DzeBjsKGIotEy2inuIXX+b0uh6SqRwb4grpbJKk/VPMT8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.13.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@poppinss/colors": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz",
|
||||
@@ -1372,6 +1361,16 @@
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@sugoidogo/node-web-globals": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://gitea.sugoidogo.com/api/packages/sugoidogo/npm/%40sugoidogo%2Fnode-web-globals/-/2.0.0/node-web-globals-2.0.0.tgz",
|
||||
"integrity": "sha512-bPFW3kBVj5pmshivjLQE/ZWKs4+6hx2rQc7YSgzK9RX48pjAeJ9/9p5/45M3Wg4KIxNi+8Jz7WpIntcDYovuHA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"undici-types": "^7.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/hast": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||||
@@ -1413,12 +1412,47 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/web": {
|
||||
"version": "0.0.329",
|
||||
"resolved": "https://registry.npmjs.org/@types/web/-/web-0.0.329.tgz",
|
||||
"integrity": "sha512-bN6arw3cJ1P607brIP16VVP7TA+jrMA5RdV1bEbi9UshLSzLQ8EVM4AsZo5/hhGRbB3n9eExbCem+GUohXv+Ng==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
"node_modules/@xmcl/curseforge": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@xmcl/curseforge/-/curseforge-2.2.0.tgz",
|
||||
"integrity": "sha512-W1b92Z1IYItIyplq2poWP1NL47buE4LOHWEU+duHCj0xWHI5y4ya2pIyXQhNzsBCSJ+ZZBXzPx/27hBl2PUwcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici": "7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@xmcl/curseforge/node_modules/undici": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.1.1.tgz",
|
||||
"integrity": "sha512-WZkQ6eH9f5ZT93gaIffsbUaDpBwjbpvmMbfaEhOnbdUneurTESeRxwPGwjI28mRFESH3W3e8Togijh37ptOQqA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@xmcl/modrinth": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xmcl/modrinth/-/modrinth-2.5.0.tgz",
|
||||
"integrity": "sha512-OpPpBemZ/yYvphgLVXGV+jESR9n8OceEsMaMvxuFZrIpi9LdYTa2oyVlnZHD7Dte+S+ttD8Y2AUncSJUNFRgNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici": "7.2.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@xmcl/modrinth/node_modules/undici": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.2.3.tgz",
|
||||
"integrity": "sha512-2oSLHaDalSt2/O/wHA9M+/ZPAOcU2yrSP/cdBYJ+YxZskiPYDSqHbysLSlD7gq3JMqOoJI5O31RVU3BxX/MnAA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
@@ -1434,13 +1468,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
|
||||
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
@@ -2150,6 +2184,7 @@
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
|
||||
+3
-2
@@ -29,9 +29,10 @@
|
||||
"docs": "typedoc src/lib/packwiz.ts --tsconfig src/lib/tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modrinth-ts/lib": "^2.1.0",
|
||||
"@sugoidogo/js-util": "^0.3.1",
|
||||
"@sugoidogo/node-file-system-adapter": "^1.0.3",
|
||||
"@xmcl/curseforge": "^2.2.0",
|
||||
"@xmcl/modrinth": "^2.5.0",
|
||||
"commander": "^14.0.3",
|
||||
"confbox": "^0.2.4",
|
||||
"curseforge-v2": "^1.5.0",
|
||||
@@ -42,9 +43,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sugoidogo/importable-types-web": "^1.0.0",
|
||||
"@sugoidogo/node-web-globals": "^2.0.0",
|
||||
"@types/md5": "^2.3.6",
|
||||
"@types/node": "^25.2.2",
|
||||
"@types/web": "^0.0.329",
|
||||
"deno": "^2.6.8",
|
||||
"typedoc": "^0.28.16",
|
||||
"typescript": "^5.9.3",
|
||||
|
||||
+31
-816
@@ -1,365 +1,8 @@
|
||||
/**
|
||||
* The hash algorithm used to determine if a file is valid.
|
||||
* All functions listed must be supported by tools implementing the packwiz pack format.
|
||||
*/
|
||||
export type HashFormat = "sha256" | "sha512" | "sha1" | "md5" | "murmur2"
|
||||
/**
|
||||
* A relative path using forward slashes.
|
||||
* Must not reference a file outside the pack root,
|
||||
* and should not include characters or filenames restricted on common operating systems.
|
||||
*
|
||||
* Implementations must support special characters including those that are percent-encoded in URLs,
|
||||
* such as spaces and square brackets.
|
||||
* Implementations must guard against path traversal attacks and manually validate paths.
|
||||
*/
|
||||
export type Path = string
|
||||
/**
|
||||
* Binary hashes should be stored as hexadecimal,
|
||||
* and case should be ignored during parsing.
|
||||
* Numeric hashes (e.g. Murmur2) should still be stored as a string,
|
||||
* to ensure the value is preserved correctly.
|
||||
*/
|
||||
export type Hash = string
|
||||
/**
|
||||
* A physical Minecraft side.
|
||||
* Server applies to the dedicated server,
|
||||
* client applies to the client (and integrated server),
|
||||
* and both applies to every installation.
|
||||
*/
|
||||
export type Side = "both" | "client" | "server"
|
||||
/**
|
||||
* An absolute URI compliant with RFC 3986.
|
||||
* Implementations may need to be more lenient in accepting reserved characters in paths
|
||||
* due to historical implementation bugs.
|
||||
* Only the HTTP/HTTPS protocols must be supported,
|
||||
* other protocols should not be used.
|
||||
*/
|
||||
export type Url = string
|
||||
/**
|
||||
* The main modpack file for a packwiz modpack.
|
||||
* This is the first file loaded,
|
||||
* to allow the modpack downloader to download all the files in the modpack.
|
||||
*/
|
||||
export interface PackMetaData {
|
||||
/**
|
||||
* The name of the modpack.
|
||||
* This can be displayed in user interfaces to identify the pack,
|
||||
* and it does not need to be unique between packs.
|
||||
*/
|
||||
"name": string
|
||||
/**
|
||||
* The author(s) of the modpack.
|
||||
* This is output when exporting to the CurseForge pack format,
|
||||
* and can be displayed in user interfaces.
|
||||
*/
|
||||
"author"?: string
|
||||
/**
|
||||
* The version of the modpack.
|
||||
* This is output when exporting to the CurseForge pack format,
|
||||
* but is not currently used elsewhere by the tools or installer.
|
||||
* It must not be used for determining if the modpack is outdated.
|
||||
*/
|
||||
"version"?: string
|
||||
/**
|
||||
* A short description of the modpack.
|
||||
* This is output when exporting to the Modrinth pack format,
|
||||
* but is not currently used elsewhere by the tools or installer.
|
||||
*/
|
||||
"description"?: string
|
||||
/**
|
||||
* A version string identifying the pack format and version of it.
|
||||
* Currently, this pack format uses version 1.1.0.
|
||||
*
|
||||
* If it is not defined, default to "packwiz:1.0.0"
|
||||
* for backwards-compatibility with packs created before this field was added.
|
||||
*
|
||||
* If it is defined:
|
||||
*
|
||||
* - All consumers should fail to load the modpack if it does not begin with "packwiz:"
|
||||
*
|
||||
* - All consumers should fail to load the modpack if the latter section is not valid semver
|
||||
* as defined in https://semver.org/spec/v2.0.0.html
|
||||
*
|
||||
* - All consumers should fail to load the modpack if the major version is greater than the version they support
|
||||
*
|
||||
* - Consumers can suggest updating themselves if the minor version is greater than the version they implement
|
||||
*
|
||||
* - Pack tools should suggest and support migration when they support a version newer than this field
|
||||
*/
|
||||
"pack-format": string
|
||||
/** Information about the index file in this modpack. */
|
||||
"index": {
|
||||
/** The path to the file that contains the index. @see {@link Path} */
|
||||
"file": Path
|
||||
/** The hash format for the hash of the index file. @see {@link HashFormat} */
|
||||
"hash-format"?: HashFormat
|
||||
/** The hash of the index file, as a string. @see {@link Hash} */
|
||||
"hash"?: Hash
|
||||
}
|
||||
/**
|
||||
* The versions of components used by this modpack - usually Minecraft and the mod loader this pack uses.
|
||||
* The existence of a component implies that it should be installed.
|
||||
* These values can also be used by tools to determine which versions of mods should be installed.
|
||||
*/
|
||||
"versions": {
|
||||
/**
|
||||
* The version of Minecraft used by this modpack.
|
||||
* This should be in the format used by the version.json files.
|
||||
*/
|
||||
"minecraft": string
|
||||
/**
|
||||
* The version of Forge used by this modpack.
|
||||
* This version must not include the Minecraft version as a prefix.
|
||||
*/
|
||||
"forge"?: string
|
||||
/** The version of Fabric loader used by this modpack. */
|
||||
"fabric"?: string
|
||||
/** The version of Quilt loader used by this modpack. */
|
||||
"quilt"?: string
|
||||
/** The version of Liteloader used by this modpack. */
|
||||
"liteloader"?: string
|
||||
"neoforge"?: string
|
||||
},
|
||||
/** Additional options */
|
||||
"options"?: {
|
||||
/**
|
||||
* A list of additional Minecraft versions to accept when installing or updating mods.
|
||||
* @see https://packwiz.infra.link/tutorials/creating/adding-mods/
|
||||
*/
|
||||
"acceptable-game-versions"?: string[]
|
||||
/**
|
||||
* The folder in which new metadata files will be added,
|
||||
* defaulting to a folder based on the category
|
||||
* (mods, resourcepacks, etc; if the category is unknown the current directory is used)
|
||||
*/
|
||||
"meta-folder"?: Path
|
||||
/** deprecated; aliassed to meta-folder */
|
||||
"mods-folder"?: Path
|
||||
/**
|
||||
* The base folder from which meta-folder will be resolved,
|
||||
* defaulting to the current directory
|
||||
* (so you can put all mods/etc in a subfolder while still using the default behaviour)
|
||||
*/
|
||||
"meta-folder-base"?: Path
|
||||
/**
|
||||
* If this is set to true,
|
||||
* packwiz will not generate hashes of local files,
|
||||
* to prevent merge conflicts and inconsistent hashes when using git/etc.
|
||||
*
|
||||
* - `packwiz refresh --build` can be used in this mode to generate internal hashes
|
||||
* for distributing the pack with packwiz-installer
|
||||
*/
|
||||
"no-internal-hashes"?: boolean
|
||||
/**
|
||||
* The folder in which datapacks are to be added;
|
||||
* specific to the datapack loader mod you use,
|
||||
* and must be set to add datapacks
|
||||
* (that are not bundled as mods)
|
||||
*/
|
||||
"datapack-folder"?: Path
|
||||
}
|
||||
}
|
||||
/**
|
||||
* The index file of the modpack,
|
||||
* storing references to every file to be downloaded in the pack.
|
||||
*/
|
||||
export interface PackIndex {
|
||||
/** The default hash format for every file in the index. @see {@link HashFormat} */
|
||||
"hash-format"?: HashFormat
|
||||
/**
|
||||
* The files listed in this index.
|
||||
* If it is not defined, defaults to an empty list.
|
||||
*/
|
||||
"files"?: [{
|
||||
/**
|
||||
* The path to the file to be downloaded,
|
||||
* relative to this index file.
|
||||
* @see {@link Path}
|
||||
*/
|
||||
"file": Path
|
||||
/** The hash of the specified file, as a string. @see {@link Hash} */
|
||||
"hash"?: Hash
|
||||
/**
|
||||
* The name with which this file should be downloaded,
|
||||
* instead of the filename specified in the path.
|
||||
* Not compatible with metafile,
|
||||
* and may not be very well supported.
|
||||
*/
|
||||
"alias"?: Path
|
||||
/**
|
||||
* The hash format for the hash of the specified file.
|
||||
* Defaults to the hash format specified in the index -
|
||||
* ideally remove this value if it is equal to the hash format for the index to save space.
|
||||
* @see {@link HashFormat}
|
||||
*/
|
||||
"hash-format"?: HashFormat
|
||||
/**
|
||||
* True when this entry points to a .toml metadata file,
|
||||
* which references a file outside the pack.
|
||||
*/
|
||||
"metafile"?: boolean
|
||||
/**
|
||||
* When this is set to true,
|
||||
* the file is not overwritten if it already exists,
|
||||
* to preserve changes made by a user.
|
||||
*/
|
||||
"preserve"?: boolean
|
||||
}]
|
||||
}
|
||||
/**
|
||||
* A metadata file which references an external file from a URL.
|
||||
* This allows for side-only mods,
|
||||
* optional mods and stores metadata to allow finding updates from Modrinth and CurseForge.
|
||||
* The "mod" terminology is used a lot here,
|
||||
* but this should work for any file.
|
||||
*/
|
||||
export interface PackFileMetaData {
|
||||
/**
|
||||
* The destination path of the mod file,
|
||||
* relative to this file.
|
||||
* @see {@link Path}
|
||||
*/
|
||||
"filename": Path
|
||||
/**
|
||||
* The name of the mod,
|
||||
* which can be displayed in user interfaces to identify the mod.
|
||||
* It does not need to be unique between mods,
|
||||
* although this may cause confusion.
|
||||
*/
|
||||
"name": string
|
||||
/** The side on which this mod should be installed. @see {@link Side} */
|
||||
"side"?: Side
|
||||
/** Information about how to download this mod. */
|
||||
"download": {
|
||||
/** The hash format for the hash of the specified file. @see {@link HashFormat} */
|
||||
"hash-format": HashFormat
|
||||
/** The hash of the specified file, as a string. @see {@link Hash} */
|
||||
"hash": Hash
|
||||
/** The URL to download the mod from. @see {@link Url} */
|
||||
"url"?: Url
|
||||
"mode"?: string
|
||||
}
|
||||
/**
|
||||
* Information about the optional state of this mod.
|
||||
* When excluded, this indicates that the mod is not optional.
|
||||
*/
|
||||
"option"?: {
|
||||
/**
|
||||
* Whether or not the mod is optional.
|
||||
* This can be set to false if you want to keep the description but make the mod required.
|
||||
*/
|
||||
"optional": boolean
|
||||
/**
|
||||
* If true, the mod will be enabled by default.
|
||||
* If false, the mod will be disabled by default.
|
||||
* If a pack format does not support optional mods but it does support disabling mods,
|
||||
* the mod will be disabled if it defaults to being disabled.
|
||||
*/
|
||||
"default"?: boolean
|
||||
/**
|
||||
* A description displayed to the user when they select optional mods.
|
||||
* This should explain why or why not the user should enable the mod.
|
||||
*/
|
||||
"description"?: string
|
||||
}
|
||||
/**
|
||||
* Information about how to update the download details of this mod with tools.
|
||||
*
|
||||
* If this object does not exist or there are no defined update sources,
|
||||
* the mod will not be automatically updated.
|
||||
*
|
||||
* If there are multiple defined update sources,
|
||||
* one of them will be chosen.
|
||||
* The source that is chosen is not defined,
|
||||
* so it is therefore dependent on the implementation of the tool
|
||||
* (may not be deterministic, so do not rely on one source being chosen over another).
|
||||
*/
|
||||
"update"?: {
|
||||
/** An update source for updating mods downloaded from CurseForge. */
|
||||
"curseforge"?: {
|
||||
/**
|
||||
* An integer representing the unique file ID of this mod file.
|
||||
* This can be used if more metadata needs to be obtained relating to the mod.
|
||||
*/
|
||||
"file-id": number
|
||||
/**
|
||||
* An integer representing the unique project ID of this mod.
|
||||
* Updating will retrieve the latest file for this project ID that is valid
|
||||
* (correct Minecraft version, release channel, modloader, etc.).
|
||||
*/
|
||||
"project-id": number
|
||||
}
|
||||
/** An update source for updating mods downloaded from Modrinth. */
|
||||
"modrinth"?: {
|
||||
/**
|
||||
* A string representing the unique mod ID of this mod.
|
||||
* Updating will retrieve the latest file for this project ID that is valid
|
||||
* (correct Minecraft version, release channel, modloader, etc.).
|
||||
*/
|
||||
"mod-id": string
|
||||
/**
|
||||
* A string representing the unique version ID of this file.
|
||||
* This can be used if more metadata needs to be obtained relating to the mod.
|
||||
*/
|
||||
"version": string
|
||||
}
|
||||
}
|
||||
}
|
||||
import type { FileSystemDirectoryHandle, FileSystemGetDirectoryOptions, FileSystemGetFileOptions } from "@sugoidogo/importable-types-web"
|
||||
import type { PackFileMetaData, PackIndex, PackMetaData, Path, Url } from "./types"
|
||||
import { parseTOML } from "confbox"
|
||||
|
||||
import WorkerlessPool from 'workerless'
|
||||
import { parseTOML, stringifyTOML } from 'confbox'
|
||||
import { CFV2Client } from 'curseforge-v2'
|
||||
import { forAsync } from '@sugoidogo/js-util'
|
||||
import * as modrinth from '@modrinth-ts/lib/dist/index'
|
||||
|
||||
import type {
|
||||
Crypto,
|
||||
FileSystemDirectoryHandle,
|
||||
FileSystemGetDirectoryOptions,
|
||||
FileSystemGetFileOptions,
|
||||
TextDecoder,
|
||||
TextEncoder,
|
||||
Console
|
||||
} from '@sugoidogo/importable-types-web'
|
||||
|
||||
declare var TextEncoder: {
|
||||
prototype: TextEncoder;
|
||||
new(): TextEncoder;
|
||||
};
|
||||
|
||||
declare var TextDecoder: {
|
||||
prototype: TextDecoder
|
||||
new(): TextDecoder
|
||||
}
|
||||
|
||||
declare var crypto: Crypto
|
||||
declare var console: Console
|
||||
|
||||
export async function getHash(data: Uint8Array<ArrayBuffer> | string, format: HashFormat): Promise<string | number> {
|
||||
if (typeof data === 'string') data = new TextEncoder().encode(data)
|
||||
if (format.startsWith('sha')) { // copied from https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API/Non-cryptographic_uses_of_subtle_crypto#hashing_a_file
|
||||
const hashAsArrayBuffer = await crypto.subtle.digest("SHA-" + format.substring(3), data);
|
||||
const uint8ViewOfHash = new Uint8Array(hashAsArrayBuffer);
|
||||
// @ts-ignore
|
||||
if (uint8ViewOfHash.toHex) {
|
||||
// @ts-ignore
|
||||
return uint8ViewOfHash.toHex() as string;
|
||||
}
|
||||
const hashAsString = Array.from(uint8ViewOfHash)
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
return hashAsString;
|
||||
}
|
||||
if (format === "murmur2") {
|
||||
const { default: murmurHash } = await import('murmur2')
|
||||
return murmurHash(data, 1, true) as number
|
||||
}
|
||||
if (format === "md5") {
|
||||
const md5 = await import('md5')
|
||||
return md5(data)
|
||||
}
|
||||
throw new Error('unknown hash format: ' + format)
|
||||
}
|
||||
const pathSeperatorRegex = /[\\\/]/
|
||||
|
||||
function splitPath(path: string) {
|
||||
path = path.replaceAll('\\', '/')
|
||||
@@ -387,464 +30,36 @@ async function getFileHandle(dir: FileSystemDirectoryHandle, path: string, optio
|
||||
return dir.getFileHandle(basename, options)
|
||||
}
|
||||
|
||||
async function removeEntry(dir: FileSystemDirectoryHandle, path: string) {
|
||||
const { dirname, basename } = splitPath(path)
|
||||
dir = await getDirectoryHandle(dir, dirname)
|
||||
return dir.removeEntry(basename)
|
||||
|
||||
async function getBytes(origin: FileSystemDirectoryHandle | Url, filePath: Path) {
|
||||
if (typeof origin === "string") {
|
||||
const url=new URL(origin)
|
||||
if (!url.pathname.endsWith(filePath)) {
|
||||
const pathSegments = url.pathname.split(pathSeperatorRegex)
|
||||
if (pathSegments.at(-1) === "") pathSegments.pop()
|
||||
pathSegments.push(filePath)
|
||||
url.pathname=pathSegments.join("/")
|
||||
}
|
||||
return fetch(url).then(response=>response.bytes())
|
||||
}
|
||||
return getFileHandle(origin, filePath)
|
||||
.then(fileHandle => fileHandle.getFile())
|
||||
.then(file=>file.bytes())
|
||||
}
|
||||
|
||||
class Modpack {
|
||||
pack_directory_handle: FileSystemDirectoryHandle
|
||||
pack_file_name: string
|
||||
pack: PackMetaData
|
||||
index: PackIndex
|
||||
files: { [key: Path]: PackFileMetaData }
|
||||
constructor({ pack_file_name = 'pack.toml', pack_directory_handle }: {
|
||||
pack_file_name?: string, pack_directory_handle?: FileSystemDirectoryHandle
|
||||
}) {
|
||||
this.pack_file_name = pack_file_name
|
||||
this.pack_directory_handle = pack_directory_handle
|
||||
}
|
||||
// initialize this modpack with default values
|
||||
async init() {
|
||||
this.index = {
|
||||
'hash-format': 'sha256'
|
||||
}
|
||||
this.pack = {
|
||||
'name': 'modpack',
|
||||
'pack-format': 'packwiz:1.1.0',
|
||||
'index': {
|
||||
'file': 'index.toml',
|
||||
'hash-format': 'sha256',
|
||||
'hash': await getHash((stringifyTOML(this.index)), 'sha256') as string
|
||||
},
|
||||
'versions': {
|
||||
'minecraft': '1.21.11' // TODO get latest minecraft version
|
||||
}
|
||||
}
|
||||
this.files = {}
|
||||
}
|
||||
// load modpack, rebuilding index
|
||||
async refresh({ pack_file_name, pack_directory_handle, useHashes = false }: {
|
||||
pack_file_name?: string, pack_directory_handle?: FileSystemDirectoryHandle, useHashes?: boolean
|
||||
}) {
|
||||
this.init()
|
||||
pack_directory_handle = pack_directory_handle || this.pack_directory_handle
|
||||
pack_file_name = pack_file_name || this.pack_file_name
|
||||
const textDecoder = new TextDecoder()
|
||||
const pack_file_handle = await pack_directory_handle.getFileHandle(pack_file_name)
|
||||
{
|
||||
const file = await pack_file_handle.getFile()
|
||||
const text = await file.text()
|
||||
this.pack = parseTOML(text)
|
||||
}
|
||||
{
|
||||
useHashes = useHashes || !this.pack.options || !this.pack.options['no-internal-hashes']
|
||||
this.index = useHashes ? { 'hash-format': this.pack.index['hash-format'] } : {}
|
||||
const { dirname, basename } = splitPath(this.pack.index.file)
|
||||
const index_directory_handle = await getDirectoryHandle(pack_directory_handle, dirname)
|
||||
const index_file_handle = await index_directory_handle.getFileHandle(basename)
|
||||
const directories = [index_directory_handle]
|
||||
while (directories.length !== 0) {
|
||||
await forAsync(directories.pop().values(), async handle => {
|
||||
if (handle.kind === 'directory') return directories.push(handle)
|
||||
if (handle.isSameEntry(index_file_handle) || handle.isSameEntry(pack_file_handle)) return
|
||||
const file = await handle.getFile()
|
||||
const bytes = await file.bytes()
|
||||
const path = (await index_directory_handle.resolve(handle)).join('/')
|
||||
const isMetaFile = handle.name.endsWith('.pw.toml')
|
||||
this.index.files.push({
|
||||
'file': path,
|
||||
'metafile': isMetaFile,
|
||||
'hash': useHashes ? await getHash(bytes, this.index['hash-format']) as string : undefined
|
||||
})
|
||||
if (isMetaFile) this.files[path] = parseTOML(textDecoder.decode(bytes))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// load modpack from storage
|
||||
async load({ pack_file_name, pack_directory_handle }: {
|
||||
pack_file_name?: string, pack_directory_handle?: FileSystemDirectoryHandle
|
||||
}) {
|
||||
this.init()
|
||||
pack_file_name = pack_file_name || this.pack_file_name
|
||||
pack_directory_handle = pack_directory_handle || this.pack_directory_handle
|
||||
const textDecoder = new TextDecoder()
|
||||
{
|
||||
const pack_file_handle = await pack_directory_handle.getFileHandle(pack_file_name)
|
||||
const file = await pack_file_handle.getFile()
|
||||
const text = await file.text()
|
||||
this.pack = parseTOML(text)
|
||||
}
|
||||
{
|
||||
const { dirname, basename } = splitPath(this.pack.index.file)
|
||||
const index_directory_handle = await getDirectoryHandle(pack_directory_handle, dirname)
|
||||
const index_file_handle = await index_directory_handle.getFileHandle(basename)
|
||||
const file = await index_file_handle.getFile()
|
||||
const bytes = await file.bytes()
|
||||
if (
|
||||
this.pack.options &&
|
||||
this.pack.options['no-internal-hashes'] &&
|
||||
await getHash(bytes, this.pack.index['hash-format']) !== this.pack.index.hash
|
||||
) throw new Error('hash validation failed on ' + file.name)
|
||||
const text = textDecoder.decode(bytes)
|
||||
this.index = parseTOML(text)
|
||||
await forAsync(this.index.files, async entry => {
|
||||
if (!entry.metafile) return
|
||||
const file_handle = await getFileHandle(index_directory_handle, entry.file)
|
||||
const file = await file_handle.getFile()
|
||||
const bytes = await file.bytes()
|
||||
if (
|
||||
this.pack.options &&
|
||||
this.pack.options['no-internal-hashes'] &&
|
||||
await getHash(bytes, entry['hash-format']) !== entry.hash
|
||||
) throw new Error('hash validation failed on ' + file.name)
|
||||
const text = textDecoder.decode(bytes)
|
||||
this.files[entry.file] = parseTOML(text)
|
||||
})
|
||||
}
|
||||
}
|
||||
// save modpack to storage
|
||||
async save({ pack_file_name, pack_directory_handle }: {
|
||||
pack_file_name?: string, pack_directory_handle?: FileSystemDirectoryHandle
|
||||
}) {
|
||||
pack_file_name = pack_file_name || this.pack_file_name
|
||||
pack_directory_handle = pack_directory_handle || this.pack_directory_handle
|
||||
{
|
||||
const pack_file_handle = await pack_directory_handle.getFileHandle(pack_file_name, { create: true })
|
||||
const writeable = await pack_file_handle.createWritable()
|
||||
await writeable.write(stringifyTOML(this.pack))
|
||||
await writeable.close()
|
||||
}
|
||||
{
|
||||
const { dirname, basename } = splitPath(this.pack.index.file)
|
||||
const index_directory_handle = await getDirectoryHandle(pack_directory_handle, dirname, { create: true })
|
||||
const index_file_handle = await index_directory_handle.getFileHandle(basename)
|
||||
const writeable = await index_file_handle.createWritable()
|
||||
writeable.write(stringifyTOML(this.index))
|
||||
writeable.close()
|
||||
await forAsync(Object.entries(this.files), async ([path, mod]) => {
|
||||
const mod_file_handle = await getFileHandle(index_directory_handle, path, { create: true })
|
||||
const writeable = await mod_file_handle.createWritable()
|
||||
await writeable.write(stringifyTOML(mod))
|
||||
await writeable.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
// find and replace files availible on curseforge
|
||||
async cfDetect({ pack_file_name, pack_directory_handle, api_key, size_min = 4096 }: {
|
||||
pack_file_name?: string, pack_directory_handle?: FileSystemDirectoryHandle,
|
||||
api_key: string, size_min?: number
|
||||
}) {
|
||||
pack_file_name = pack_file_name || this.pack_file_name
|
||||
pack_directory_handle = pack_directory_handle || this.pack_directory_handle
|
||||
const { dirname, basename } = splitPath(this.pack.index.file)
|
||||
const index_directory_handle = await getDirectoryHandle(pack_directory_handle, dirname)
|
||||
const workerlessPool = new WorkerlessPool()
|
||||
const file_hash_map = new Map<number, Path>()
|
||||
await forAsync(this.index.files, async indexEntry => {
|
||||
if (indexEntry.metafile) return
|
||||
const file_handle = await getFileHandle(index_directory_handle, indexEntry.file)
|
||||
const file = await file_handle.getFile()
|
||||
if (file.size < size_min) return
|
||||
const bytes = file.bytes()
|
||||
const hash = Number(await workerlessPool.run(getHash, bytes, 'murmur2'))
|
||||
|
||||
})
|
||||
workerlessPool.terminate()
|
||||
console.log('requesting matches from curseforge')
|
||||
const response = await new CFV2Client({ apiKey: api_key }).getFingerprintMatches({ 'fingerprints': Array.from(file_hash_map.keys()) })
|
||||
const textEncoder = new TextEncoder()
|
||||
let resultCount = 0
|
||||
await forAsync(Object.values(response.data.data.exactMatches), async function (match) {
|
||||
let file_path = file_hash_map.get(match.file.fileFingerprint)
|
||||
if (!file_path) {
|
||||
for (const module of match.file.modules) {
|
||||
file_path = file_hash_map.get(module.fingerprint)
|
||||
}
|
||||
if (file_path) {
|
||||
console.debug("skipping module match, this is probably a false positive", file_path, match.file)
|
||||
} else {
|
||||
console.debug("curseforge returned a result we didn't ask for, skipping", match.file)
|
||||
}
|
||||
return
|
||||
}
|
||||
const { dirname: file_dir_name, basename: file_name } = splitPath(file_path)
|
||||
for (const hash of match.file.hashes) {
|
||||
if (hash.algo === 1) {
|
||||
const mod: PackFileMetaData = {
|
||||
"name": match.file.displayName,
|
||||
"filename": match.file.fileName,
|
||||
"download": {
|
||||
"hash-format": "sha1",
|
||||
"hash": hash.value,
|
||||
"mode": "metadata:curseforge"
|
||||
},
|
||||
"update": {
|
||||
"curseforge": {
|
||||
"file-id": match.file.id,
|
||||
"project-id": match.file.modId
|
||||
}
|
||||
}
|
||||
}
|
||||
const new_file_path = file_dir_name + '/' + encodeURI(match.file.displayName.toLowerCase().replaceAll(' ', '-') + '.pw.toml')
|
||||
const new_file_data = textEncoder.encode(stringifyTOML(mod))
|
||||
const new_file_hash = (await getHash(new_file_data, this.index['hash-format'])).toString()
|
||||
await getFileHandle(index_directory_handle, new_file_path, { 'create': true })
|
||||
.then(handle => handle.createWritable())
|
||||
.then(stream => {
|
||||
stream.write(new_file_data)
|
||||
stream.close()
|
||||
this.index.files.push({
|
||||
"file": new_file_path,
|
||||
"hash": new_file_hash.toString(),
|
||||
"metafile": true
|
||||
})
|
||||
})
|
||||
await removeEntry(index_directory_handle, file_path)
|
||||
.then(() => {
|
||||
for (let i = 0; i < this.index.files.length; i++) {
|
||||
if (this.index.files[i].file != file_path) continue
|
||||
this.index.files.splice(i, 1)
|
||||
return
|
||||
}
|
||||
throw new Error("can't find removed file in index")
|
||||
})
|
||||
resultCount++
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
const index_file_data = textEncoder.encode(stringifyTOML(this.index))
|
||||
const index_file_hash = await getHash(index_file_data, this.pack.index['hash-format'])
|
||||
const index_file_handle = await getFileHandle(index_directory_handle, basename)
|
||||
const pack_file_handle = await getFileHandle(pack_directory_handle, this.pack_file_name)
|
||||
await index_file_handle.createWritable()
|
||||
.then(async stream => {
|
||||
await stream.write(index_file_data)
|
||||
await stream.close()
|
||||
this.pack.index.hash = index_file_hash.toString()
|
||||
return pack_file_handle.createWritable()
|
||||
}).then(async stream => {
|
||||
await stream.write(stringifyTOML(this.pack))
|
||||
await stream.close()
|
||||
})
|
||||
console.log(`found ${resultCount} matching files`)
|
||||
}
|
||||
// detect and replace files availible on modrinth
|
||||
async mrDetect({ pack_file_name, pack_directory_handle, api_key, size_min = 4096 }: {
|
||||
pack_file_name?: string, pack_directory_handle?: FileSystemDirectoryHandle,
|
||||
api_key: string, size_min?: number
|
||||
}) {
|
||||
pack_file_name = pack_file_name || this.pack_file_name
|
||||
pack_directory_handle = pack_directory_handle || this.pack_directory_handle
|
||||
const { dirname, basename } = splitPath(this.pack.index.file)
|
||||
const index_directory_handle = await getDirectoryHandle(pack_directory_handle, dirname)
|
||||
const file_hash_map = new Map<Hash, Path>()
|
||||
console.log('hashing files')
|
||||
await forAsync(this.index.files, async function (entry) {
|
||||
if (entry.metafile) return
|
||||
const file = await getFileHandle(index_directory_handle, entry.file)
|
||||
.then(handle => handle.getFile())
|
||||
if (file.size < size_min) return
|
||||
const file_data = await file.bytes()
|
||||
const hash = await getHash(file_data, "sha1") as string
|
||||
file_hash_map.set(hash, entry.file)
|
||||
})
|
||||
console.log('requesting matches from modrinth')
|
||||
const matches = await modrinth.getLatestVersionsFromHashes({
|
||||
'auth': api_key,
|
||||
'body': {
|
||||
"algorithm": "sha1",
|
||||
"hashes": Array.from(file_hash_map.keys()),
|
||||
"game_versions": [],
|
||||
"loaders": []
|
||||
}
|
||||
})
|
||||
const version_matches = new Map<string, modrinth.Version>()
|
||||
for (const match of Object.values(matches.data)) version_matches.set(match.project_id, match)
|
||||
const projects = await modrinth.getProjects({
|
||||
'auth': api_key,
|
||||
query: {
|
||||
ids: JSON.stringify(Array.from(version_matches.keys()))
|
||||
}
|
||||
})
|
||||
const textEncoder = new TextEncoder()
|
||||
let resultCount = 0
|
||||
await forAsync(projects.data, async function (project) {
|
||||
const version = version_matches.get(project.id)
|
||||
if (!version) {
|
||||
console.debug("modrinth returned a project result we didn't ask for, skipping", project)
|
||||
return
|
||||
}
|
||||
const file_path = file_hash_map.get(version.files[0].hashes.sha1)
|
||||
if (!file_path) {
|
||||
console.debug("modrinth returned a file result we didn't ask for, skipping", version)
|
||||
return
|
||||
}
|
||||
let side: Side = "both"
|
||||
if (project.client_side === "unsupported") side = "server"
|
||||
if (project.server_side === "unsupported") side = "client"
|
||||
const mod: PackFileMetaData = {
|
||||
"name": project.title,
|
||||
"filename": version.files[0].filename,
|
||||
"side": side,
|
||||
"download": {
|
||||
"hash-format": "sha1",
|
||||
"hash": version.files[0].hashes.sha1,
|
||||
"url": version.files[0].url
|
||||
},
|
||||
"update": {
|
||||
"modrinth": {
|
||||
"version": version.id,
|
||||
"mod-id": version.project_id
|
||||
}
|
||||
}
|
||||
}
|
||||
const new_file_path = splitPath(file_path).dirname + '/' + encodeURI(project.title.toLowerCase().replaceAll(' ', '-') + '.pw.toml')
|
||||
const new_file_data = textEncoder.encode(stringifyTOML(mod))
|
||||
const new_file_hash = await getHash(new_file_data, this.index['hash-format'])
|
||||
await getFileHandle(index_directory_handle, new_file_path, { 'create': true })
|
||||
.then(handle => handle.createWritable())
|
||||
.then(stream => {
|
||||
stream.write(new_file_data)
|
||||
stream.close()
|
||||
this.index.files.push({
|
||||
"file": new_file_path,
|
||||
"hash": new_file_hash,
|
||||
"metafile": true
|
||||
})
|
||||
})
|
||||
await removeEntry(index_directory_handle, file_path)
|
||||
.then(() => {
|
||||
for (let i = 0; i < this.index.files.length; i++) {
|
||||
if (this.index.files[i].file != file_path) continue
|
||||
this.index.files.splice(i, 1)
|
||||
return
|
||||
}
|
||||
throw new Error("can't find removed file in index")
|
||||
})
|
||||
resultCount++
|
||||
})
|
||||
const index_file_data = textEncoder.encode(stringifyTOML(this.index))
|
||||
const index_file_hash = await getHash(index_file_data, this.pack.index['hash-format'])
|
||||
const index_file_handle = await getFileHandle(index_directory_handle, basename)
|
||||
const pack_file_handle = await getFileHandle(pack_directory_handle, pack_file_name)
|
||||
await index_file_handle.createWritable()
|
||||
.then(async stream => {
|
||||
await stream.write(index_file_data)
|
||||
await stream.close()
|
||||
this.pack.index.hash = index_file_hash.toString()
|
||||
return pack_file_handle.createWritable()
|
||||
}).then(async stream => {
|
||||
await stream.write(stringifyTOML(this.pack))
|
||||
await stream.close()
|
||||
})
|
||||
console.log(`found ${resultCount} matching files`)
|
||||
}
|
||||
// cache download urls for curseforge files
|
||||
async cfUrl({ api_key }: { api_key: string }) {
|
||||
const mod_id_paths: Map<number,Path>=new Map()
|
||||
for (const [path, mod] of Object.entries(this.files)) {
|
||||
if (mod.download.mode !== 'metadata:curseforge') continue
|
||||
mod_id_paths.set(Number(mod.update.curseforge['file-id']),path)
|
||||
}
|
||||
console.log(`requesting ${mod_id_paths.size} download urls`)
|
||||
const result = await new CFV2Client({ 'apiKey': api_key }).getFiles({ 'fileIds': Array.from(mod_id_paths.keys()) })
|
||||
if (result.statusCode != 200) {
|
||||
throw new Error(result.message)
|
||||
}
|
||||
const files = result.data.data
|
||||
let blockedFileCount=0
|
||||
for (const file of files) {
|
||||
if (!file.downloadUrl) {
|
||||
console.warn('third party downloads disabled for ' + file.displayName)
|
||||
blockedFileCount++
|
||||
continue
|
||||
}
|
||||
const path = mod_id_paths.get(file.id)!
|
||||
delete this.files[path].download.mode
|
||||
this.files[path].download.url = file.downloadUrl
|
||||
}
|
||||
console.log(`found ${files.length-blockedFileCount} download urls`)
|
||||
}
|
||||
export interface PackwizPack {
|
||||
packOrigin: FileSystemDirectoryHandle | Url
|
||||
packFileName: Path
|
||||
packMetaData: PackMetaData
|
||||
packIndex: PackIndex
|
||||
packMetaFiles: { [key: Path]: PackFileMetaData | undefined }
|
||||
}
|
||||
|
||||
/**
|
||||
* find curseforge files that are availible on modrinth and merge their metadata
|
||||
* @param pack_url url or path to pack.toml
|
||||
* @param mrApiKey modrinth api key
|
||||
*/
|
||||
export async function mrMerge(pack_url: string, mrApiKey?: string): Promise<Result> {
|
||||
console.log('loading pack')
|
||||
const pack = await fetch_toml(pack_url) as PackMetaData
|
||||
const pack_dir = dirname(pack_url)
|
||||
const index_url = `${pack_dir}/${pack.index.file}`
|
||||
console.log('loading index')
|
||||
const index = await fetch_toml(index_url) as PackIndex
|
||||
const index_dir = dirname(index_url)
|
||||
if (!index.files) {
|
||||
console.warn(`${index_url} has no files indexed`)
|
||||
return {}
|
||||
export async function loadPack(origin: FileSystemDirectoryHandle | Url, packFileName = 'pack.toml') {
|
||||
const packMetaData:PackMetaData = await getBytes(origin, packFileName).then(bytes => parseTOML(bytes.toString()))
|
||||
const packIndex: PackIndex = await getBytes(origin, packMetaData.index.file).then(bytes => parseTOML(bytes.toString()))
|
||||
const {dirname:indexDirPath}=splitPath(packMetaData.index.file)
|
||||
for (const entry of packIndex.files) {
|
||||
// TODO
|
||||
}
|
||||
console.log('loading curseforge files')
|
||||
const file_hash_map: any = {}
|
||||
await Promise.all(index.files.map(async function (file) {
|
||||
if (!file.metafile) return
|
||||
const file_url = `${index_dir}/${file.file}`
|
||||
const mod = await fetch_toml(file_url) as PackFileMetaData
|
||||
if (mod.update && mod.update.modrinth) return
|
||||
if (!file_hash_map[mod.download['hash-format']]) file_hash_map[mod.download['hash-format']] = {}
|
||||
file_hash_map[mod.download['hash-format']][mod.download.hash] = { file_url, mod }
|
||||
}))
|
||||
console.log('requesting matches from modrinth')
|
||||
const project_matches = new Map<string, any>()
|
||||
for (const hash_format in file_hash_map) {
|
||||
const matches = await fetch_json('https://api.modrinth.com/v2/version_files', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'accept': 'application/json',
|
||||
'authorization': mrApiKey
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"algorithm": hash_format,
|
||||
"hashes": Array.from(Object.keys(file_hash_map[hash_format]))
|
||||
})
|
||||
})
|
||||
for (const match of Object.values(matches) as any[]) project_matches.set(match.project_id, match)
|
||||
}
|
||||
const projects = await fetch_json(`https://api.modrinth.com/v2/projects?ids=["${Array.from(project_matches.keys()).join('","')}"]`)
|
||||
const result: Result = { write: {} }
|
||||
for (const project of projects) {
|
||||
const match = project_matches.get(project.id)
|
||||
if (!match) {
|
||||
console.debug("modrinth returned a project result we didn't ask for, skipping", project)
|
||||
continue
|
||||
}
|
||||
let file_url: Path | undefined
|
||||
let mod: PackFileMetaData | undefined
|
||||
for (const hash_format in file_hash_map) {
|
||||
({ file_url, mod } = file_hash_map[hash_format][match.files[0].hashes[hash_format].toLowerCase().replace('-', '')])
|
||||
}
|
||||
if (!file_url || !mod) {
|
||||
console.debug("modrinth returned a file result we didn't ask for, skipping", match)
|
||||
continue
|
||||
}
|
||||
delete mod.download.mode
|
||||
mod.download.url = match.files[0].url
|
||||
let side: Side = "both"
|
||||
if (project.client_side === "unsupported") side = "server"
|
||||
if (project.server_side === "unsupported") side = "client"
|
||||
mod.side = side
|
||||
if (!mod.update) mod.update = {}
|
||||
mod.update.modrinth = {
|
||||
"version": match.id,
|
||||
"mod-id": match.project_id
|
||||
}
|
||||
result.write[file_url] = mod
|
||||
}
|
||||
console.log(`found ${Object.keys(result.write).length} matching files`)
|
||||
return result
|
||||
}
|
||||
@@ -8,12 +8,9 @@
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "../../dist",
|
||||
"types": [],
|
||||
"types": ["@sugoidogo/node-web-globals"],
|
||||
"rewriteRelativeImportExtensions": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"verbatimModuleSyntax": true
|
||||
},
|
||||
"include": [
|
||||
"packwiz.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
Vendored
+309
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* The hash algorithm used to determine if a file is valid.
|
||||
* All functions listed must be supported by tools implementing the packwiz pack format.
|
||||
*/
|
||||
export type HashFormat = "sha256" | "sha512" | "sha1" | "md5" | "murmur2"
|
||||
/**
|
||||
* A relative path using forward slashes.
|
||||
* Must not reference a file outside the pack root,
|
||||
* and should not include characters or filenames restricted on common operating systems.
|
||||
*
|
||||
* Implementations must support special characters including those that are percent-encoded in URLs,
|
||||
* such as spaces and square brackets.
|
||||
* Implementations must guard against path traversal attacks and manually validate paths.
|
||||
*/
|
||||
export type Path = string
|
||||
/**
|
||||
* Binary hashes should be stored as hexadecimal,
|
||||
* and case should be ignored during parsing.
|
||||
* Numeric hashes (e.g. Murmur2) should still be stored as a string,
|
||||
* to ensure the value is preserved correctly.
|
||||
*/
|
||||
export type Hash = string
|
||||
/**
|
||||
* A physical Minecraft side.
|
||||
* Server applies to the dedicated server,
|
||||
* client applies to the client (and integrated server),
|
||||
* and both applies to every installation.
|
||||
*/
|
||||
export type Side = "both" | "client" | "server"
|
||||
/**
|
||||
* An absolute URI compliant with RFC 3986.
|
||||
* Implementations may need to be more lenient in accepting reserved characters in paths
|
||||
* due to historical implementation bugs.
|
||||
* Only the HTTP/HTTPS protocols must be supported,
|
||||
* other protocols should not be used.
|
||||
*/
|
||||
export type Url = string
|
||||
/**
|
||||
* The main modpack file for a packwiz modpack.
|
||||
* This is the first file loaded,
|
||||
* to allow the modpack downloader to download all the files in the modpack.
|
||||
*/
|
||||
export interface PackMetaData {
|
||||
/**
|
||||
* The name of the modpack.
|
||||
* This can be displayed in user interfaces to identify the pack,
|
||||
* and it does not need to be unique between packs.
|
||||
*/
|
||||
"name": string
|
||||
/**
|
||||
* The author(s) of the modpack.
|
||||
* This is output when exporting to the CurseForge pack format,
|
||||
* and can be displayed in user interfaces.
|
||||
*/
|
||||
"author"?: string
|
||||
/**
|
||||
* The version of the modpack.
|
||||
* This is output when exporting to the CurseForge pack format,
|
||||
* but is not currently used elsewhere by the tools or installer.
|
||||
* It must not be used for determining if the modpack is outdated.
|
||||
*/
|
||||
"version"?: string
|
||||
/**
|
||||
* A short description of the modpack.
|
||||
* This is output when exporting to the Modrinth pack format,
|
||||
* but is not currently used elsewhere by the tools or installer.
|
||||
*/
|
||||
"description"?: string
|
||||
/**
|
||||
* A version string identifying the pack format and version of it.
|
||||
* Currently, this pack format uses version 1.1.0.
|
||||
*
|
||||
* If it is not defined, default to "packwiz:1.0.0"
|
||||
* for backwards-compatibility with packs created before this field was added.
|
||||
*
|
||||
* If it is defined:
|
||||
*
|
||||
* - All consumers should fail to load the modpack if it does not begin with "packwiz:"
|
||||
*
|
||||
* - All consumers should fail to load the modpack if the latter section is not valid semver
|
||||
* as defined in https://semver.org/spec/v2.0.0.html
|
||||
*
|
||||
* - All consumers should fail to load the modpack if the major version is greater than the version they support
|
||||
*
|
||||
* - Consumers can suggest updating themselves if the minor version is greater than the version they implement
|
||||
*
|
||||
* - Pack tools should suggest and support migration when they support a version newer than this field
|
||||
*/
|
||||
"pack-format": string
|
||||
/** Information about the index file in this modpack. */
|
||||
"index": {
|
||||
/** The path to the file that contains the index. @see {@link Path} */
|
||||
"file": Path
|
||||
/** The hash format for the hash of the index file. @see {@link HashFormat} */
|
||||
"hash-format"?: HashFormat
|
||||
/** The hash of the index file, as a string. @see {@link Hash} */
|
||||
"hash"?: Hash
|
||||
}
|
||||
/**
|
||||
* The versions of components used by this modpack - usually Minecraft and the mod loader this pack uses.
|
||||
* The existence of a component implies that it should be installed.
|
||||
* These values can also be used by tools to determine which versions of mods should be installed.
|
||||
*/
|
||||
"versions": {
|
||||
/**
|
||||
* The version of Minecraft used by this modpack.
|
||||
* This should be in the format used by the version.json files.
|
||||
*/
|
||||
"minecraft": string
|
||||
/**
|
||||
* The version of Forge used by this modpack.
|
||||
* This version must not include the Minecraft version as a prefix.
|
||||
*/
|
||||
"forge"?: string
|
||||
/** The version of Fabric loader used by this modpack. */
|
||||
"fabric"?: string
|
||||
/** The version of Quilt loader used by this modpack. */
|
||||
"quilt"?: string
|
||||
/** The version of Liteloader used by this modpack. */
|
||||
"liteloader"?: string
|
||||
"neoforge"?: string
|
||||
},
|
||||
/** Additional options */
|
||||
"options"?: {
|
||||
/**
|
||||
* A list of additional Minecraft versions to accept when installing or updating mods.
|
||||
* @see https://packwiz.infra.link/tutorials/creating/adding-mods/
|
||||
*/
|
||||
"acceptable-game-versions"?: string[]
|
||||
/**
|
||||
* The folder in which new metadata files will be added,
|
||||
* defaulting to a folder based on the category
|
||||
* (mods, resourcepacks, etc; if the category is unknown the current directory is used)
|
||||
*/
|
||||
"meta-folder"?: Path
|
||||
/** deprecated; aliassed to meta-folder */
|
||||
"mods-folder"?: Path
|
||||
/**
|
||||
* The base folder from which meta-folder will be resolved,
|
||||
* defaulting to the current directory
|
||||
* (so you can put all mods/etc in a subfolder while still using the default behaviour)
|
||||
* @see {@link Path}
|
||||
*/
|
||||
"meta-folder-base"?: Path
|
||||
/**
|
||||
* If this is set to true,
|
||||
* packwiz will not generate hashes of local files,
|
||||
* to prevent merge conflicts and inconsistent hashes when using git/etc.
|
||||
*
|
||||
* - `packwiz refresh --build` can be used in this mode to generate internal hashes
|
||||
* for distributing the pack with packwiz-installer
|
||||
*/
|
||||
"no-internal-hashes"?: boolean
|
||||
/**
|
||||
* The folder in which datapacks are to be added;
|
||||
* specific to the datapack loader mod you use,
|
||||
* and must be set to add datapacks
|
||||
* (that are not bundled as mods)
|
||||
* @see {@link Path}
|
||||
*/
|
||||
"datapack-folder"?: Path
|
||||
}
|
||||
}
|
||||
/**
|
||||
* The index file of the modpack,
|
||||
* storing references to every file to be downloaded in the pack.
|
||||
*/
|
||||
export interface PackIndex {
|
||||
/** The default hash format for every file in the index. @see {@link HashFormat} */
|
||||
"hash-format"?: HashFormat
|
||||
/**
|
||||
* The files listed in this index.
|
||||
* If it is not defined, defaults to an empty list.
|
||||
*/
|
||||
"files"?: [{
|
||||
/**
|
||||
* The path to the file to be downloaded,
|
||||
* relative to this index file.
|
||||
* @see {@link Path}
|
||||
*/
|
||||
"file": Path
|
||||
/** The hash of the specified file, as a string. @see {@link Hash} */
|
||||
"hash"?: Hash
|
||||
/**
|
||||
* The name with which this file should be downloaded,
|
||||
* instead of the filename specified in the path.
|
||||
* Not compatible with metafile,
|
||||
* and may not be very well supported.
|
||||
*/
|
||||
"alias"?: Path
|
||||
/**
|
||||
* The hash format for the hash of the specified file.
|
||||
* Defaults to the hash format specified in the index -
|
||||
* ideally remove this value if it is equal to the hash format for the index to save space.
|
||||
* @see {@link HashFormat}
|
||||
*/
|
||||
"hash-format"?: HashFormat
|
||||
/**
|
||||
* True when this entry points to a .toml metadata file,
|
||||
* which references a file outside the pack.
|
||||
*/
|
||||
"metafile"?: boolean
|
||||
/**
|
||||
* When this is set to true,
|
||||
* the file is not overwritten if it already exists,
|
||||
* to preserve changes made by a user.
|
||||
*/
|
||||
"preserve"?: boolean
|
||||
}]
|
||||
}
|
||||
/**
|
||||
* A metadata file which references an external file from a URL.
|
||||
* This allows for side-only mods,
|
||||
* optional mods and stores metadata to allow finding updates from Modrinth and CurseForge.
|
||||
* The "mod" terminology is used a lot here,
|
||||
* but this should work for any file.
|
||||
*/
|
||||
export interface PackFileMetaData {
|
||||
/**
|
||||
* The destination path of the mod file,
|
||||
* relative to this file.
|
||||
* @see {@link Path}
|
||||
*/
|
||||
"filename": Path
|
||||
/**
|
||||
* The name of the mod,
|
||||
* which can be displayed in user interfaces to identify the mod.
|
||||
* It does not need to be unique between mods,
|
||||
* although this may cause confusion.
|
||||
*/
|
||||
"name": string
|
||||
/** The side on which this mod should be installed. @see {@link Side} */
|
||||
"side"?: Side
|
||||
/** Information about how to download this mod. */
|
||||
"download": {
|
||||
/** The hash format for the hash of the specified file. @see {@link HashFormat} */
|
||||
"hash-format": HashFormat
|
||||
/** The hash of the specified file, as a string. @see {@link Hash} */
|
||||
"hash": Hash
|
||||
/** The URL to download the mod from. @see {@link Url} */
|
||||
"url"?: Url
|
||||
"mode"?: string
|
||||
}
|
||||
/**
|
||||
* Information about the optional state of this mod.
|
||||
* When excluded, this indicates that the mod is not optional.
|
||||
*/
|
||||
"option"?: {
|
||||
/**
|
||||
* Whether or not the mod is optional.
|
||||
* This can be set to false if you want to keep the description but make the mod required.
|
||||
*/
|
||||
"optional": boolean
|
||||
/**
|
||||
* If true, the mod will be enabled by default.
|
||||
* If false, the mod will be disabled by default.
|
||||
* If a pack format does not support optional mods but it does support disabling mods,
|
||||
* the mod will be disabled if it defaults to being disabled.
|
||||
*/
|
||||
"default"?: boolean
|
||||
/**
|
||||
* A description displayed to the user when they select optional mods.
|
||||
* This should explain why or why not the user should enable the mod.
|
||||
*/
|
||||
"description"?: string
|
||||
}
|
||||
/**
|
||||
* Information about how to update the download details of this mod with tools.
|
||||
*
|
||||
* If this object does not exist or there are no defined update sources,
|
||||
* the mod will not be automatically updated.
|
||||
*
|
||||
* If there are multiple defined update sources,
|
||||
* one of them will be chosen.
|
||||
* The source that is chosen is not defined,
|
||||
* so it is therefore dependent on the implementation of the tool
|
||||
* (may not be deterministic, so do not rely on one source being chosen over another).
|
||||
*/
|
||||
"update"?: {
|
||||
/** An update source for updating mods downloaded from CurseForge. */
|
||||
"curseforge"?: {
|
||||
/**
|
||||
* An integer representing the unique file ID of this mod file.
|
||||
* This can be used if more metadata needs to be obtained relating to the mod.
|
||||
*/
|
||||
"file-id": number
|
||||
/**
|
||||
* An integer representing the unique project ID of this mod.
|
||||
* Updating will retrieve the latest file for this project ID that is valid
|
||||
* (correct Minecraft version, release channel, modloader, etc.).
|
||||
*/
|
||||
"project-id": number
|
||||
}
|
||||
/** An update source for updating mods downloaded from Modrinth. */
|
||||
"modrinth"?: {
|
||||
/**
|
||||
* A string representing the unique mod ID of this mod.
|
||||
* Updating will retrieve the latest file for this project ID that is valid
|
||||
* (correct Minecraft version, release channel, modloader, etc.).
|
||||
*/
|
||||
"mod-id": string
|
||||
/**
|
||||
* A string representing the unique version ID of this file.
|
||||
* This can be used if more metadata needs to be obtained relating to the mod.
|
||||
*/
|
||||
"version": string
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user