diff --git a/bun.lock b/bun.lock index fa21322..fa80c1e 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,7 @@ "": { "name": "packwizjs", "dependencies": { + "curseforge-api": "^1.3.0", "toml": "^3.0.0", }, "devDependencies": { @@ -109,6 +110,8 @@ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "curseforge-api": ["curseforge-api@1.3.0", "", {}, "sha512-MxIFAtmzCBmZaDUF7bDs8wPa8mAr1qneT9YKw1pVTjW0+cWGYop05Qm+ZEoAVToVQPYcNFHA2GoBPNX48zo+aQ=="], + "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], diff --git a/package.json b/package.json index 9f009b6..b1e62ac 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "typescript": "^5.0.0" }, "dependencies": { + "curseforge-api": "^1.3.0", "toml": "^3.0.0" } } diff --git a/src/parser.ts b/src/parser.ts index bcf7459..c66a5e8 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,55 +1,109 @@ -import * as fs from "fs"; import * as toml from "toml"; -import FilePath from "./path"; +import { CurseForgeClient } from "curseforge-api"; +import Resource from "./resource"; + +// const CURSE_CLIENT = new CurseForgeClient(); export interface Metafile { name: string; filename: string; side: "server" | "client" | "both"; - provider: ModrinthProvider | CurseForgeProvider; + provider: Provider; } -export interface ModrinthProvider { - url: string; - hashFormat: string; - hash: string; - modId: string; - versionId: string; +export abstract class Provider { + constructor( + public hash: string, + public hashFormat: string, + ) {} + + abstract getDownloadUrl(): Promise; } -export interface CurseForgeProvider { - hashFormat: string; - hash: string; - mode: string; - fileId: number; - projectId: number; +export class UrlProvider extends Provider { + constructor( + public hash: string, + public hashFormat: string, + public url: string, + ) { + super(hash, hashFormat); + } + + async getDownloadUrl(): Promise { + return new Resource(this.url); + } +} + +export class ModrinthProvider extends UrlProvider { + constructor( + hash: string, + hashFormat: string, + url: string, + public modId: string, + public versionId: string, + ) { + super(hash, hashFormat, url); + } +} + +export class GitHubProvider extends UrlProvider { + constructor( + hash: string, + hashFormat: string, + url: string, + public branch: string, + public regex: string, + public slug: string, + public tag: string, + ) { + super(hash, hashFormat, url); + } +} + +export class CurseForgeProvider extends Provider { + constructor( + public hash: string, + public hashFormat: string, + public mode: string, + public fileId: number, + public projectId: number, + ) { + super(hash, hashFormat); + } + + async getDownloadUrl(): Promise { + // const mod = await CURSE_CLIENT.getMod(this.projectId); + // const file = await mod.getFile(this.fileId); + // return new Resource(await file.getDownloadURL()); + return new Resource("https://google.com/search?q=curseforge+sucks"); // TODO: figure this out, i hate curseforge + } } /** * Represents a file entry in the Packwiz index. */ export class IndexFileEntry { - readonly file: FilePath; + readonly file: Resource; readonly hash: string; readonly metafile: boolean; constructor( - file: string | FilePath, + file: string | Resource, hash: string, metafile: boolean = false, ) { - this.file = file instanceof FilePath ? file : new FilePath(file); + this.file = file instanceof Resource ? file : new Resource(file); this.hash = hash; this.metafile = metafile; } - parse(): Metafile { + async parse(): Promise { if (this.metafile === false) { throw new Error( "Cannot parse non-metafiles! Use `file.readText()` instead to get the file contents.", ); } - const fileContent = this.file.readText(); + const fileContent = await this.file.fetchContents(); const parsed = toml.parse(fileContent); const metafile: Metafile = { @@ -62,23 +116,29 @@ export class IndexFileEntry { return metafile; } - private parseProvider(parsed: any): ModrinthProvider | CurseForgeProvider { + private parseProvider(parsed: any): Provider { if (parsed.update?.modrinth) { - return { - url: parsed.download.url, - hashFormat: parsed.download["hash-format"], - hash: parsed.download.hash, - modId: parsed.update.modrinth["mod-id"], - versionId: parsed.update.modrinth["version"], - }; + return new ModrinthProvider( + parsed.download.url, + parsed.download["hash-format"], + parsed.download.hash, + parsed.update.modrinth["mod-id"], + parsed.update.modrinth["version"], + ); } else if (parsed.update?.curseforge) { - return { - hashFormat: parsed.download["hash-format"], - hash: parsed.download.hash, - mode: parsed.download.mode, - fileId: parsed.update.curseforge["file-id"], - projectId: parsed.update.curseforge["project-id"], - }; + return new CurseForgeProvider( + parsed.download["hash-format"], + parsed.download.hash, + parsed.download.mode, + parsed.update.curseforge["file-id"], + parsed.update.curseforge["project-id"], + ); + } else if (parsed.download) { + return new UrlProvider( + parsed.download.hash, + parsed.download["hash-format"], + parsed.download.url, + ); } else { throw new Error("Unknown provider in TOML."); } @@ -89,7 +149,7 @@ export class IndexFileEntry { * Represents the structure of a Packwiz index file, as well as providing its location. */ export interface PackwizIndex { - location: FilePath; + location: Resource; hashFormat: string; files: IndexFileEntry[]; } @@ -103,9 +163,11 @@ export interface PackwizIndex { * @param indexFilePath - The path of the TOML file. * @returns The parsed index as a structured object. */ -export function parsePackwizIndex(indexFilePath: string): PackwizIndex { - const indexFile = new FilePath(indexFilePath); - const parsed = toml.parse(indexFile.readText()); +export async function parsePackwizIndex( + indexFilePath: string, +): Promise { + const indexFile = new Resource(indexFilePath); + const parsed = toml.parse(await indexFile.fetchContents()); return { location: indexFile, hashFormat: parsed["hash-format"], diff --git a/src/path.ts b/src/path.ts deleted file mode 100644 index 807c9e9..0000000 --- a/src/path.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; - -export default class FilePath { - constructor(public readonly path: string) {} - - toString() { - return this.path; - } - - get name() { - return path.basename(this.path); - } - - get parent() { - return new FilePath(path.dirname(this.path)); - } - - get ext() { - return path.extname(this.path); - } - - exists() { - return fs.existsSync(this.path); - } - - readText(): string { - return fs.readFileSync(this.path, "utf-8"); - } - - // writeText(content: string) { - // fs.writeFileSync(this.path, content, "utf-8"); - // } - - join(...segments: string[]) { - return new FilePath(path.join(this.path, ...segments)); - } -} diff --git a/src/resource.ts b/src/resource.ts new file mode 100644 index 0000000..4d7637d --- /dev/null +++ b/src/resource.ts @@ -0,0 +1,70 @@ +import * as fs from "fs/promises"; +import { URL } from "url"; +import * as path from "path"; + +export default class Resource { + constructor(public readonly path: string) {} + + toString() { + return this.path; + } + + get isUrl() { + try { + new URL(this.path); + return true; + } catch { + return false; + } + } + + get name() { + return this.isUrl + ? new URL(this.path).pathname.split("/").pop() || "" + : path.basename(this.path); + } + + get parent() { + 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)); + } + + get ext() { + return this.isUrl + ? path.extname(new URL(this.path).pathname) + : path.extname(this.path); + } + + async exists(): Promise { + if (this.isUrl) { + const response = await fetch(this.path); + if (!response.ok) return false; + return true; + } else { + return fs.exists(this.path); + } + } + + async fetchContents(): Promise { + 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" }); + } + + 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)); + } +}