Phase5
This commit is contained in:
70
packages/catalog/src/compatibility.test.ts
Normal file
70
packages/catalog/src/compatibility.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
isVersionCompatible,
|
||||
pickPrimaryFile,
|
||||
resolveInstallDirectory,
|
||||
resolveModrinthLoader,
|
||||
resolveProjectType,
|
||||
} from './compatibility';
|
||||
import type { ModrinthVersion } from './modrinth';
|
||||
|
||||
describe('catalog compatibility', () => {
|
||||
it('maps paper to plugin installs', () => {
|
||||
expect(resolveModrinthLoader('PAPER')).toBe('paper');
|
||||
expect(resolveProjectType('PAPER')).toBe('plugin');
|
||||
expect(resolveInstallDirectory('plugin')).toBe('plugins');
|
||||
});
|
||||
|
||||
it('maps fabric to mod installs', () => {
|
||||
expect(resolveModrinthLoader('FABRIC')).toBe('fabric');
|
||||
expect(resolveProjectType('FABRIC')).toBe('mod');
|
||||
expect(resolveInstallDirectory('mod')).toBe('mods');
|
||||
});
|
||||
|
||||
it('checks version compatibility', () => {
|
||||
const version: ModrinthVersion = {
|
||||
id: 'v1',
|
||||
project_id: 'p1',
|
||||
name: '1.0.0',
|
||||
version_number: '1.0.0',
|
||||
game_versions: ['1.21.1'],
|
||||
loaders: ['paper'],
|
||||
files: [],
|
||||
date_published: '2026-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
expect(isVersionCompatible(version, '1.21.1', 'paper')).toBe(true);
|
||||
expect(isVersionCompatible(version, '1.20.1', 'paper')).toBe(false);
|
||||
});
|
||||
|
||||
it('picks primary jar file', () => {
|
||||
const version: ModrinthVersion = {
|
||||
id: 'v1',
|
||||
project_id: 'p1',
|
||||
name: '1.0.0',
|
||||
version_number: '1.0.0',
|
||||
game_versions: ['1.21.1'],
|
||||
loaders: ['fabric'],
|
||||
files: [
|
||||
{
|
||||
filename: 'readme.txt',
|
||||
url: 'https://example.com/readme.txt',
|
||||
hashes: {},
|
||||
primary: false,
|
||||
size: 10,
|
||||
},
|
||||
{
|
||||
filename: 'mod.jar',
|
||||
url: 'https://example.com/mod.jar',
|
||||
hashes: { sha512: 'abc' },
|
||||
primary: true,
|
||||
size: 100,
|
||||
},
|
||||
],
|
||||
date_published: '2026-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
expect(pickPrimaryFile(version)?.filename).toBe('mod.jar');
|
||||
});
|
||||
});
|
||||
75
packages/catalog/src/compatibility.ts
Normal file
75
packages/catalog/src/compatibility.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { ModrinthProjectType, ModrinthVersion, SoftwareFamily } from './modrinth';
|
||||
|
||||
export function resolveModrinthLoader(
|
||||
softwareFamily: SoftwareFamily,
|
||||
): string | null {
|
||||
switch (softwareFamily) {
|
||||
case 'PAPER':
|
||||
case 'PURPUR':
|
||||
return 'paper';
|
||||
case 'FABRIC':
|
||||
return 'fabric';
|
||||
case 'FORGE':
|
||||
return 'forge';
|
||||
case 'NEOFORGE':
|
||||
return 'neoforge';
|
||||
case 'QUILT':
|
||||
return 'quilt';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveProjectType(
|
||||
softwareFamily: SoftwareFamily,
|
||||
): ModrinthProjectType | null {
|
||||
switch (softwareFamily) {
|
||||
case 'PAPER':
|
||||
case 'PURPUR':
|
||||
return 'plugin';
|
||||
case 'FABRIC':
|
||||
case 'FORGE':
|
||||
case 'NEOFORGE':
|
||||
case 'QUILT':
|
||||
return 'mod';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveInstallDirectory(projectType: ModrinthProjectType): string {
|
||||
switch (projectType) {
|
||||
case 'plugin':
|
||||
return 'plugins';
|
||||
case 'mod':
|
||||
return 'mods';
|
||||
case 'modpack':
|
||||
return 'modpacks';
|
||||
default:
|
||||
return 'datapacks';
|
||||
}
|
||||
}
|
||||
|
||||
export function isVersionCompatible(
|
||||
version: ModrinthVersion,
|
||||
gameVersion: string,
|
||||
loader: string,
|
||||
): boolean {
|
||||
const gameMatch = version.game_versions.some(
|
||||
(value) => value === gameVersion || gameVersion.startsWith(value),
|
||||
);
|
||||
const loaderMatch = version.loaders.includes(loader);
|
||||
return gameMatch && loaderMatch;
|
||||
}
|
||||
|
||||
export function pickPrimaryFile(version: ModrinthVersion): ModrinthVersion['files'][number] | null {
|
||||
const jarFiles = version.files.filter((file) =>
|
||||
file.filename.toLowerCase().endsWith('.jar'),
|
||||
);
|
||||
|
||||
if (jarFiles.length === 0) {
|
||||
return version.files[0] ?? null;
|
||||
}
|
||||
|
||||
return jarFiles.find((file) => file.primary) ?? jarFiles[0] ?? null;
|
||||
}
|
||||
20
packages/catalog/src/index.ts
Normal file
20
packages/catalog/src/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export {
|
||||
searchModrinthProjects,
|
||||
getModrinthProject,
|
||||
getModrinthProjectVersions,
|
||||
getModrinthVersion,
|
||||
type ModrinthProject,
|
||||
type ModrinthSearchResponse,
|
||||
type ModrinthVersion,
|
||||
type ModrinthVersionFile,
|
||||
type ModrinthProjectType,
|
||||
type SoftwareFamily,
|
||||
} from './modrinth';
|
||||
|
||||
export {
|
||||
resolveModrinthLoader,
|
||||
resolveProjectType,
|
||||
resolveInstallDirectory,
|
||||
isVersionCompatible,
|
||||
pickPrimaryFile,
|
||||
} from './compatibility';
|
||||
148
packages/catalog/src/modrinth.ts
Normal file
148
packages/catalog/src/modrinth.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
export type SoftwareFamily =
|
||||
| 'VANILLA'
|
||||
| 'PAPER'
|
||||
| 'PURPUR'
|
||||
| 'FABRIC'
|
||||
| 'FORGE'
|
||||
| 'NEOFORGE'
|
||||
| 'QUILT'
|
||||
| 'BEDROCK_DEDICATED';
|
||||
|
||||
export type ModrinthProjectType = 'mod' | 'plugin' | 'modpack' | 'resourcepack' | 'shader';
|
||||
|
||||
export interface ModrinthProject {
|
||||
project_id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
project_type: ModrinthProjectType;
|
||||
downloads: number;
|
||||
icon_url?: string;
|
||||
}
|
||||
|
||||
export interface ModrinthSearchResponse {
|
||||
hits: ModrinthProject[];
|
||||
offset: number;
|
||||
limit: number;
|
||||
total_hits: number;
|
||||
}
|
||||
|
||||
export interface ModrinthVersionFile {
|
||||
hashes: Record<string, string>;
|
||||
url: string;
|
||||
filename: string;
|
||||
primary: boolean;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface ModrinthVersion {
|
||||
id: string;
|
||||
project_id: string;
|
||||
name: string;
|
||||
version_number: string;
|
||||
game_versions: string[];
|
||||
loaders: string[];
|
||||
files: ModrinthVersionFile[];
|
||||
date_published: string;
|
||||
}
|
||||
|
||||
const MODRINTH_API = 'https://api.modrinth.com/v2';
|
||||
|
||||
export async function searchModrinthProjects(input: {
|
||||
query: string;
|
||||
projectType: ModrinthProjectType;
|
||||
gameVersion: string;
|
||||
loader?: string;
|
||||
limit?: number;
|
||||
}): Promise<ModrinthSearchResponse> {
|
||||
const facets: string[][] = [
|
||||
[`project_type:${input.projectType}`],
|
||||
[`versions:${input.gameVersion}`],
|
||||
];
|
||||
|
||||
if (input.loader) {
|
||||
facets.push([`categories:${input.loader}`]);
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
query: input.query,
|
||||
limit: String(input.limit ?? 20),
|
||||
index: 'relevance',
|
||||
facets: JSON.stringify(facets),
|
||||
});
|
||||
|
||||
const response = await fetch(`${MODRINTH_API}/search?${params.toString()}`, {
|
||||
headers: {
|
||||
'User-Agent': 'HexaHost-GameCloud/1.0 (catalog-sync)',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Modrinth search failed: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json() as Promise<ModrinthSearchResponse>;
|
||||
}
|
||||
|
||||
export async function getModrinthProjectVersions(
|
||||
projectId: string,
|
||||
): Promise<ModrinthVersion[]> {
|
||||
const response = await fetch(
|
||||
`${MODRINTH_API}/project/${encodeURIComponent(projectId)}/version`,
|
||||
{
|
||||
headers: {
|
||||
'User-Agent': 'HexaHost-GameCloud/1.0 (catalog-sync)',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Modrinth versions failed: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json() as Promise<ModrinthVersion[]>;
|
||||
}
|
||||
|
||||
export interface ModrinthProjectDetails {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
project_type: ModrinthProjectType;
|
||||
downloads: number;
|
||||
icon_url?: string;
|
||||
}
|
||||
|
||||
export async function getModrinthProject(projectId: string): Promise<ModrinthProjectDetails> {
|
||||
const response = await fetch(
|
||||
`${MODRINTH_API}/project/${encodeURIComponent(projectId)}`,
|
||||
{
|
||||
headers: {
|
||||
'User-Agent': 'HexaHost-GameCloud/1.0 (catalog-sync)',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Modrinth project failed: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json() as Promise<ModrinthProjectDetails>;
|
||||
}
|
||||
|
||||
export async function getModrinthVersion(versionId: string): Promise<ModrinthVersion> {
|
||||
const response = await fetch(
|
||||
`${MODRINTH_API}/version/${encodeURIComponent(versionId)}`,
|
||||
{
|
||||
headers: {
|
||||
'User-Agent': 'HexaHost-GameCloud/1.0 (catalog-sync)',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Modrinth version failed: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json() as Promise<ModrinthVersion>;
|
||||
}
|
||||
Reference in New Issue
Block a user