Add MajorMinorVersion type and dedupe the game version regex

Address review feedback: introduce a MajorMinorVersion type with
isMajorMinorVersion, rename majorMinorVersion to normaliseMajorMinorVersion,
and type ModInfo.factorioVersion as MajorMinorVersion so already-reduced
values are not re-reduced (ModPackViewPage uses mod.info.factorioVersion
directly). Reuse the single gameVersionRegExp in both GameVersionSchema and
normaliseGameVersion instead of duplicating the pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
bbassieandClaude Opus 4.8 committed 2026-06-29 22:12:36 +00:00
1 parent 97a6c32838
commit 7c2793752d
6 files changed
+47 -21

No files matched your search

+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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<ModStoreEvents> {
) {
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(",") }),
+4 -4
View File
@@ -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
+19 -6
View File
@@ -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<PartialVersion>(
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<MajorMinorVersion>(
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;
}
/**
@@ -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";
}
+16 -4
View File
@@ -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() {