diff --git a/packages/ctl/src/commands_mod.ts b/packages/ctl/src/commands_mod.ts index 365e62d0..343c6953 100644 --- a/packages/ctl/src/commands_mod.ts +++ b/packages/ctl/src/commands_mod.ts @@ -114,7 +114,7 @@ modCommands.add(new lib.Command({ } let response = await control.send(new lib.ModSearchRequest( args.query, - lib.majorMinorVersion(searchVersion), + lib.normaliseMajorMinorVersion(searchVersion), args.page, args.pageSize, args.sort, diff --git a/packages/lib/src/ModStore.ts b/packages/lib/src/ModStore.ts index 3bcead88..023fc528 100644 --- a/packages/lib/src/ModStore.ts +++ b/packages/lib/src/ModStore.ts @@ -5,7 +5,7 @@ import { Static } from "@sinclair/typebox"; import { logger } from "./logging"; import { downloadFile, safeOutputFile } from "./file_ops"; import { ModPortalReleaseSchema, ModPortalDetailsSchema, ModNameVersionPair } from "./data/messages_mod"; -import { ModInfo, ModPack, FullVersion, PartialVersion, majorMinorVersion, ModVersionEquality } from "./data"; +import { ModInfo, ModPack, FullVersion, PartialVersion, normaliseMajorMinorVersion, ModVersionEquality } from "./data"; export interface ModStoreEvents { /** A stored mod was created, updated or deleted */ @@ -202,7 +202,7 @@ export default class ModStore extends events.EventEmitter { ) { const url = new URL("https://mods.factorio.com/api/mods"); url.searchParams.set("page_size", "max"); - url.searchParams.set("version", majorMinorVersion(factorioVersion)); + url.searchParams.set("version", normaliseMajorMinorVersion(factorioVersion)); const response = await fetch(url, { method: "POST", body: new URLSearchParams({ namelist: mods.map(m => m.name).join(",") }), diff --git a/packages/lib/src/data/ModInfo.ts b/packages/lib/src/data/ModInfo.ts index 5d1003ed..458459c7 100644 --- a/packages/lib/src/data/ModInfo.ts +++ b/packages/lib/src/data/ModInfo.ts @@ -10,8 +10,8 @@ import { ModRecord } from "./ModPack"; import { FullVersion, integerFullVersion, integerPartialVersion, - GameVersionSchema, normaliseGameVersion, majorMinorVersion, - PartialVersion, + GameVersionSchema, normaliseGameVersion, normaliseMajorMinorVersion, + MajorMinorVersion, ModVersionEquality, } from "./version"; @@ -175,7 +175,7 @@ export default class ModInfo { * Major version of Factorio this mod supports. * Sourced from info.json. */ - factorioVersion = "0.12" as PartialVersion; + factorioVersion = "0.12" as MajorMinorVersion; /** * Integer representation of the factorioVersion @@ -297,7 +297,7 @@ export default class ModInfo { if (factorioVersion === undefined) { throw new Error(`Invalid factorio_version "${json.factorio_version}"`); } - modInfo.factorioVersion = majorMinorVersion(factorioVersion); + modInfo.factorioVersion = normaliseMajorMinorVersion(factorioVersion); } // Parse the dependencies diff --git a/packages/lib/src/data/version.ts b/packages/lib/src/data/version.ts index 230af793..dc6c925f 100644 --- a/packages/lib/src/data/version.ts +++ b/packages/lib/src/data/version.ts @@ -60,7 +60,7 @@ export function normaliseFullVersion(version: PartialVersion) { * optional, and any trailing content is ignored. The pattern is intentionally * not anchored at the end. */ -const gameVersionRegExp = /^\s*\d+\.\s*\d+(?:\.\s*\d+)?/; +const gameVersionRegExp = /^\s*(\d+)\.\s*(\d+)(?:\.\s*(\d+))?/; export const GameVersionSchema = Type.String({ pattern: gameVersionRegExp.source }); /** @@ -72,7 +72,7 @@ export const GameVersionSchema = Type.String({ pattern: gameVersionRegExp.source * could be read. */ export function normaliseGameVersion(input: string): FullVersion | undefined { - const match = /^\s*(\d+)\.\s*(\d+)(?:\.\s*(\d+))?/.exec(input); + const match = gameVersionRegExp.exec(input); if (match === null) { return undefined; } @@ -83,7 +83,7 @@ export function normaliseGameVersion(input: string): FullVersion | undefined { * Matches valid factorio versions and mod dependencies specifications where 2 or 3 parts are specified. */ const partialVersionRegExp = /^\d+\.\d+(?:\.\d+)?$/; -export type PartialVersion = FullVersion | `${number}.${number}`; +export type PartialVersion = FullVersion | MajorMinorVersion; export const PartialVersionSchema = Type.Unsafe( Type.String({ pattern: partialVersionRegExp.source }) ); @@ -99,11 +99,24 @@ export function integerPartialVersion(version: PartialVersion) { } /** - * The major.minor of a version, as used by the mod portal's version filter. + * Matches a major.minor version, as used by the mod portal's version filter. */ -export function majorMinorVersion(version: PartialVersion) { +const majorMinorVersionRegExp = /^\d+\.\d+$/; +export type MajorMinorVersion = `${number}.${number}`; +export const MajorMinorVersionSchema = Type.Unsafe( + Type.String({ pattern: majorMinorVersionRegExp.source }) +); + +export function isMajorMinorVersion(input: string): input is MajorMinorVersion { + return majorMinorVersionRegExp.test(input); +} + +/** + * Reduce a version to its major.minor, as used by the mod portal's version filter. + */ +export function normaliseMajorMinorVersion(version: PartialVersion): MajorMinorVersion { const [major, minor] = version.split("."); - return `${major}.${minor}` as `${number}.${number}`; + return `${major}.${minor}` as MajorMinorVersion; } /** diff --git a/packages/web_ui/src/components/ModPackViewPage.tsx b/packages/web_ui/src/components/ModPackViewPage.tsx index 391caf05..4a823081 100644 --- a/packages/web_ui/src/components/ModPackViewPage.tsx +++ b/packages/web_ui/src/components/ModPackViewPage.tsx @@ -82,7 +82,7 @@ function SearchModsTable(props: SearchModsTableProps) { // Get a valid factorio version useEffect(() => { try { - setFactorioVersion(lib.majorMinorVersion(props.modPack.factorioVersion)); + setFactorioVersion(lib.normaliseMajorMinorVersion(props.modPack.factorioVersion)); } catch (err) { setFactorioVersion(null); } @@ -295,7 +295,7 @@ function DownloadDependenciesButton(props: DownloadDependenciesProps) { // Get a valid factorio version useEffect(() => { try { - setFactorioVersion(lib.majorMinorVersion(props.modPack.factorioVersion)); + setFactorioVersion(lib.normaliseMajorMinorVersion(props.modPack.factorioVersion)); } catch (err) { setError(new Error(`Invalid factorio version: ${props.modPack.factorioVersion}`)); setFactorioVersion(null); @@ -577,8 +577,9 @@ function ModsTable(props: ModsTableProps) { if (mod.enabled && mod.info) { mod.warning = mod.info.checkDependencySatisfaction(mods.filter(m => m.enabled)); try { - const packFactorioVersion = lib.majorMinorVersion(props.modPack.factorioVersion); - const modFactorioVersion = lib.majorMinorVersion(mod.info.factorioVersion); + const packFactorioVersion = lib.normaliseMajorMinorVersion(props.modPack.factorioVersion); + // mod.info.factorioVersion is already a MajorMinorVersion, no need to re-reduce it. + const modFactorioVersion = mod.info.factorioVersion; if (packFactorioVersion !== modFactorioVersion) { mod.warning = "wrong_factorio_version"; } diff --git a/test/lib/data/version.test.js b/test/lib/data/version.test.js index c9fb7986..6fc05005 100644 --- a/test/lib/data/version.test.js +++ b/test/lib/data/version.test.js @@ -39,11 +39,23 @@ describe("lib/data/version", function() { } }); }); - describe("majorMinorVersion()", function() { + describe("normaliseMajorMinorVersion()", function() { it("should return the major.minor", function() { - assert.equal(lib.majorMinorVersion("1.1"), "1.1"); - assert.equal(lib.majorMinorVersion("1.1.50"), "1.1"); - assert.equal(lib.majorMinorVersion("0.18.0"), "0.18"); + assert.equal(lib.normaliseMajorMinorVersion("1.1"), "1.1"); + assert.equal(lib.normaliseMajorMinorVersion("1.1.50"), "1.1"); + assert.equal(lib.normaliseMajorMinorVersion("0.18.0"), "0.18"); + }); + }); + describe("isMajorMinorVersion()", function() { + it("should correctly validate input strings", function() { + const valid = ["1.1", "0.18", "2.0", "10.20"]; + const invalid = ["1", "1.1.0", "1.1.50", "latest", "1.", ".1", ""]; + for (const version of valid) { + assert(lib.isMajorMinorVersion(version), `${version} should be valid`); + } + for (const version of invalid) { + assert(!lib.isMajorMinorVersion(version), `${version} should be invalid`); + } }); }); describe("isFullVersion()", function() {