feat(parser): switch to monorepo format
All checks were successful
Actions / Build and Push Documentation (push) Successful in 16s

This commit is contained in:
cswimr 2025-02-11 09:03:43 -06:00
parent b6bcd4c207
commit 741d2ca999
Signed by: cswimr
GPG key ID: 0EC431A8DA8F8087
12 changed files with 74 additions and 33 deletions

View file

@ -0,0 +1,9 @@
# MIT License
Copyright (c) 2025 GalacticFactory
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,34 @@
{
"name": "@packwizjs/parser",
"module": "src/parser.ts",
"version": "1.0.2",
"type": "module",
"description": "A simple library for parsing Packwiz TOML files",
"author": {
"name": "cswimr",
"email": "seaswimmerthefsh@gmail.com"
},
"repository": {
"url": "https://c.csw.im/GalacticFactory/PackwizJS",
"type": "git"
},
"license": "MIT",
"files": [
"src"
],
"readme": "../../README.md",
"homepage": "https://packwizjs.csw.im",
"keywords": [
"packwiz",
"minecraft"
],
"scripts": {},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"@types/semver": "^7.5.8",
"semver": "^7.7.1",
"toml": "^3.0.0"
}
}

View file

@ -0,0 +1,39 @@
/**
* The hash formats allowed by Packwiz.
*/
export enum HashFormat {
/**
* SHA-256 hash format.
*/
"sha256" = "sha256",
/**
* SHA-512 hash format.
*/
"sha512" = "sha512",
/**
* SHA-1 hash format.
*/
"sha1" = "sha1",
/**
* MD5 hash format.
*/
"md5" = "md5",
/**
* Murmur2 hash format.
* This is a proprietary hash format used by CurseForge.
*/
"murmur2" = "murmur2",
}
/**
* Checks if the provided hash format is allowed by Packwiz.
* @param hashFormat - The hash format to check.
* @returns The HashFormat enum value if valid, otherwise throws an error.
*/
export function isValidHashFormat(hashFormat: string): HashFormat {
if (hashFormat in HashFormat) {
return HashFormat[hashFormat as keyof typeof HashFormat];
} else {
throw new Error(`Invalid index file hash format: ${hashFormat}`);
}
}

View file

@ -0,0 +1,34 @@
/**
* The sides allowed by Packwiz.
*/
export enum Side {
/**
* Download a file on both clients and servers.
*/
"Both" = "both",
/**
* Download a file only on clients.
*/
"Client" = "client",
/**
* Download a file only on servers.
*/
"Server" = "server",
}
/**
* Checks if the provided side is allowed by Packwiz.
* @param side - The side to check.
* @returns The Side enum value if valid, otherwise throws an error.
*/
export function isValidSide(side: string | undefined): Side {
if (side == undefined) {
return Side.Both;
}
if (Object.values(Side).includes(side.toLowerCase() as Side)) {
return side.toLowerCase() as Side;
} else {
throw new Error("Invalid side!");
}
}

View file

@ -0,0 +1,300 @@
import * as toml from "toml";
import { Resource } from "./resource";
import {
UrlProvider,
ModrinthProvider,
CurseForgeProvider,
GitHubProvider,
} from "./provider";
import { HashFormat, isValidHashFormat } from "./enums/hash-format";
import { isValidSide, type Side } from "./enums/side";
import * as semver from "semver";
/**
* The structure of a Packwiz metafile.
*/
export interface Metafile {
/**
* A human-readable name for the file.
*/
name: string;
/**
* The name that the file should have when downloaded.
*/
filename: string;
/**
* Whether the file is for the client or server.
* Consuming applications should respect this!
*/
side: Side;
/**
* The provider to use to download / update the file.
* All consuming applications should be doing with this is downloading the file.
* Updates are handled by Packwiz itself.
*/
provider: UrlProvider;
/**
* Whether the file is optional.
* If true, the end user should be prompted to download the file.
*/
isOptional: boolean;
/**
* Whether the file is the default for the mod.
* If this is true, but isOptional is also true, the end user should be asked if they want to download the file.
*/
isDefault: boolean;
/**
* The description of the file.
* This should be shown to end users.
*/
description?: string;
}
/**
* Represents a file entry in the Packwiz index.
*/
export class IndexFileEntry {
/**
* Creates a new IndexFileEntry instance.
* @param file - The Resource instance representing the file.
* @param hash - The hash of the file.
* @param hashFormat - The format of the hash.
* @param alias - An optional alias for the file that will be downloaded by this file.
* @param metafile - Whether the file is a metafile.
* @param preserve - Whether to preserve the file downloaded by this file if it is modified by an end user.
*/
constructor(
readonly file: Resource,
readonly hash: string,
readonly hashFormat: HashFormat,
readonly alias?: string,
readonly metafile: boolean = false,
readonly preserve: boolean = false,
) {
this.file = file instanceof Resource ? file : new Resource(file);
this.hash = hash;
this.hashFormat = isValidHashFormat(hashFormat);
this.alias = alias;
this.metafile = metafile;
this.preserve = preserve;
}
/**
* Parses the TOML file and returns its contents.
* @returns The parsed file as a structured object.
*/
async parse(): Promise<Metafile> {
if (this.metafile === false) {
throw new Error(
"Cannot parse non-metafiles! Use `file.fetchContents()` instead to get the file contents.",
);
}
const fileContent = await this.file.fetchContents();
const parsed = toml.parse(fileContent);
const side = isValidSide(parsed.side);
const metafile: Metafile = {
name: parsed.name,
filename: parsed.filename,
side: side,
provider: this.parseProvider(parsed),
isOptional: parsed.option?.optional || false,
isDefault: parsed.option?.default || true,
description: parsed.option?.description,
};
return metafile;
}
/**
* Parse the provider from the TOML file.
* @returns The parsed provider.
*/
private parseProvider(parsed: any): UrlProvider {
if (parsed.update?.modrinth) {
return new ModrinthProvider(
parsed.download.hash,
isValidHashFormat(parsed.download["hash-format"]),
new Resource(parsed.download.url),
parsed.update.modrinth["mod-id"],
parsed.update.modrinth["version"],
);
} else if (parsed.update?.curseforge) {
return new CurseForgeProvider(
parsed.download.hash,
isValidHashFormat(parsed.download["hash-format"]),
parsed.download.mode,
parsed.update.curseforge["file-id"],
parsed.update.curseforge["project-id"],
);
} else if (parsed.update?.github) {
return new GitHubProvider(
parsed.download.hash,
isValidHashFormat(parsed.download["hash-format"]),
new Resource(parsed.download.url),
parsed.update.github.branch,
parsed.update.github.regex,
parsed.update.github.slug,
parsed.update.github.tag,
);
} else if (parsed.download) {
return new UrlProvider(
parsed.download.hash,
isValidHashFormat(parsed.download["hash-format"]),
new Resource(parsed.download.url),
);
} else {
throw new Error("Unknown provider in TOML.");
}
}
}
/**
* Represents the structure of a Packwiz index file, as well as providing its location.
*/
export interface PackwizIndex {
/**
* The location of the index file. This can be a local file path or a remote URL.
*/
location: Resource;
/**
* The hash of the index file.
*/
hash: string;
/**
* The hash format of the index file.
*/
hashFormat: HashFormat;
/**
* The files listed in the index file.
*/
files: IndexFileEntry[];
}
/**
* Represents modloaders present in the pack.
* If any of these are not undefined, applications using this information should install the specific version of the modlaoder listed.
*/
export interface PackwizVersions {
/**
* The Minecraft version that the pack is for.
*/
minecraft: string;
/**
* The version of [Fabric](https://fabricmc.net/) that the pack targets.
*/
fabric?: string;
/**
* The version of [Forge](https://files.minecraftforge.net/) that the pack targets.
*/
forge?: string;
/**
* The version of [NeoForge](https://neoforged.net/) that the pack targets.
*/
neoforge?: string;
/**
* The version of [Quilt](https://quiltmc.org/) that the pack targets.
*/
quilt?: string;
/**
* The version of [LiteLoader](https://www.liteloader.com/) that the pack targets.
*/
liteloader?: string;
}
/**
* A class representing a Packwiz TOML file.
*/
export class Packwiz {
/**
* Creates a new Packwiz instance.
* @param location - The location of the TOML file.
* @param index - The index of the TOML file.
* @param name - The name of the pack.
* @param packFormat - The packwiz version of the TOML file.
* @param authors - The authors of the pack.
* @param description - The description of the pack.
* @param version - The version of the pack.
* @param versions - The Minecraft / modloader versions that the pack uses.
*/
constructor(
readonly location: Resource,
readonly index: PackwizIndex,
readonly name: string,
readonly packFormat: string = "packwiz:1.0.0",
readonly authors: Array<string>,
readonly description: string,
readonly version: string,
readonly versions: PackwizVersions,
) {
const packwizJsMaxSupportedVersion = "1.1.0";
const packFormatSemver = semver.clean(packFormat.split(":")[1]);
if (!packFormat.startsWith("packwiz:")) {
throw new Error("Invalid packwiz pack format!");
} else if (!packFormatSemver) {
throw new Error("Unable to parse pack format!");
} else if (semver.gt(packFormatSemver, packwizJsMaxSupportedVersion)) {
throw new Error(
`Pack format of ${packFormat} is higher than what PackwizJS supports! Please report this upstream at https://c.csw.im/GalacticFactory/PackwizJS/issues`,
);
}
}
}
/**
* Parses a packwiz.toml file and returns its contents.
*
* @param filePath - The path of the TOML file. This can also be a URL.
* @returns The parsed file as a structured class.
*/
export async function parsePackwiz(filePath: string): Promise<Packwiz> {
let packwizFile = new Resource(filePath);
// make the actual path consistent with the index file
packwizFile = packwizFile.parent.join(packwizFile.name);
const parsedPackwizFile = toml.parse(await packwizFile.fetchContents());
const indexFile = packwizFile.parent.join(parsedPackwizFile.index.file);
const parsedIndexFile = toml.parse(await indexFile.fetchContents());
const indexHashFormat = isValidHashFormat(parsedIndexFile["hash-format"]);
return new Packwiz(
packwizFile,
{
location: indexFile,
hash: parsedPackwizFile.index.hash,
hashFormat: indexHashFormat,
files: parsedIndexFile.files.map(
(file: {
file: string;
hash: string;
hashFormat: HashFormat;
alias?: string;
metafile?: boolean;
preserve?: boolean;
}) => {
return new IndexFileEntry(
packwizFile.parent.join(file.file),
file.hash,
file.hashFormat ?? indexHashFormat,
file.alias,
file.metafile ?? false,
file.preserve ?? false,
);
},
),
},
parsedPackwizFile.name,
parsedPackwizFile["pack-format"],
parsedPackwizFile.author?.split(",").map((s: string) => s.trim()),
parsedPackwizFile.description,
parsedPackwizFile.version,
{
minecraft: parsedPackwizFile.versions.minecraft,
fabric: parsedPackwizFile.versions.fabric,
forge: parsedPackwizFile.versions.forge,
neoforge: parsedPackwizFile.versions.neoforge,
quilt: parsedPackwizFile.versions.quilt,
liteloader: parsedPackwizFile.versions.liteloader,
},
);
}

View file

@ -0,0 +1,101 @@
import { Resource } from "./resource";
import { HashFormat } from "./enums/hash-format";
/**
* Provides a base class for URL-based providers.
*/
export class UrlProvider {
/**
* Creates a new UrlProvider instance.
* @param hash - A hash string used for identifying or verifying the resource.
* @param hashFormat - The format of the provided hash.
* @param url - A Resource instance representing the URL where the file can be downloaded from.
*/
constructor(
public hash: string,
public hashFormat: HashFormat,
public url: Resource,
) {}
}
/**
* URL provider for handling Modrinth-related URLs.
* Extends the UrlProvider with Modrinth-specific properties.
*/
export class ModrinthProvider extends UrlProvider {
/**
* Creates a new ModrinthProvider instance.
* @param hash - A hash string used for identification or verification.
* @param hashFormat - The format of the provided hash.
* @param url - A Resource instance representing the URL where the file can be downloaded from.
* @param modId - The identifier for the Modrinth mod.
* @param versionId - The version identifier for the mod.
*/
constructor(
hash: string,
hashFormat: HashFormat,
url: Resource,
public modId: string,
public versionId: string,
) {
super(hash, hashFormat, url);
}
}
/**
* URL provider for handling GitHub-related URLs.
* Extends the UrlProvider with GitHub-specific properties.
*/
export class GitHubProvider extends UrlProvider {
/**
* Creates a new GitHubProvider instance.
* @param hash - A hash string used for identification or verification.
* @param hashFormat - The format of the hash.
* @param url - A Resource instance representing the URL where the file can be downloaded from.
* @param branch - The branch name within the GitHub repository.
* @param regex - A regular expression for matching specific patterns in the GitHub URL.
* @param slug - A URL slug used for identifying the repository.
* @param tag - The tag associated with the GitHub resource.
*/
constructor(
hash: string,
hashFormat: HashFormat,
url: Resource,
public branch: string,
public regex: string,
public slug: string,
public tag: string,
) {
super(hash, hashFormat, url);
}
}
/**
* URL provider for handling CurseForge-related URLs.
* Extends the UrlProvider with CurseForge-specific properties.
*/
export class CurseForgeProvider extends UrlProvider {
/**
* Creates a new CurseForgeProvider instance.
* @param hash - A hash string used for identification or verification.
* @param hashFormat - The format of the provided hash.
* @param mode - The mode indicating how the CurseForge file should be accessed.
* @param fileId - The identifier for the file on CurseForge.
* @param projectId - The identifier for the project on CurseForge.
*/
constructor(
hash: string,
hashFormat: HashFormat,
public mode: string,
public fileId: number,
public projectId: number,
) {
super(
hash,
hashFormat,
new Resource(
`https://www.curseforge.com/api/v1/mods/${projectId}/files/${fileId}/download`,
),
);
}
}

View file

@ -0,0 +1,110 @@
import * as fs from "fs/promises";
import { URL } from "url";
import * as path from "path";
/**
* Resource class for handling file and URL paths.
*/
export class Resource {
/**
* Creates a new Resource instance.
* @param path - The file or URL path.
*/
constructor(public readonly path: string) {}
/**
* Returns the string representation of the Resource instance.
* @returns The path of the Resource.
*/
toString(): string {
return this.path;
}
/**
* Checks if the path is a valid URL.
* @return True if the path is a valid URL, false otherwise.
*/
get isUrl(): boolean {
try {
new URL(this.path);
return true;
} catch {
return false;
}
}
/**
* Gets the name of the file or URL.
* @return The name of the file or URL.
*/
get name(): string {
return this.isUrl
? new URL(this.path).pathname.split("/").pop() || ""
: path.basename(this.path);
}
/**
* Gets the parent directory of the file or URL.
* @return The parent directory of the file or URL.
*/
get parent(): Resource {
if (this.isUrl) {
const url = new URL(this.path);
url.pathname = path.dirname(url.pathname);
return new Resource(url.toString());
}
return new Resource(path.dirname(this.path));
}
/**
* Gets the file extension of the file or URL.
* @return The file extension of the file or URL.
*/
get ext(): string {
return this.isUrl
? path.extname(new URL(this.path).pathname)
: path.extname(this.path);
}
/**
* Checks if the file or URL exists.
* @return True if the file or URL exists, false otherwise.
*/
async exists(): Promise<boolean> {
if (this.isUrl) {
const response = await fetch(this.path);
if (!response.ok) return false;
return true;
} else {
return fs.exists(this.path);
}
}
/**
* Fetches the contents of the file or URL.
* @return The contents of file the Resource points to.
*/
async fetchContents(): Promise<string> {
if (this.isUrl) {
const response = await fetch(this.path);
if (!response.ok)
throw new Error(`Failed to fetch ${this.path}: ${response.statusText}`);
return await response.text();
}
return fs.readFile(this.path, { encoding: "utf8" });
}
/**
* Joins the Resource with other segments to create a new Resource.
* @param segments - The segments to join with the Resource.
* @return A new Resource instance with the joined path.
*/
join(...segments: string[]) {
if (this.isUrl) {
const url = new URL(this.path);
url.pathname = path.join(url.pathname, ...segments);
return new Resource(url.toString());
}
return new Resource(path.join(this.path, ...segments));
}
}

View file

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
},
"include": [
"src/**/*.ts"
],
}