Files
Hornwitser 5dcaca4d0b Migrate to ECMAScript Modules
Migrate the usage of CommonJS specific code and imports to ESM.  Roughly
summarised this:
- Changes all require() calls to import statement or expressions.
- Changes usage of module.exports to export statements.
- Replaces require.resolve with import.meta.resolve.
- Replaces conditional imports with conditional subpath imports.
- Replaces module === require.main with import.meta.main.
- Adds the full filename with extention to all relative imports.
- Changes all usage of __dirname and __filename to their import.meta
  equivalents.
- Sets "type": "module" in package.json files.
- Removes usage of "use strict".
- Adds .ts an an alias of .js for the webpack build.

Due to differences in how require and import resolves files the plugin
entrypoints will have to include the filename and extension after this
migration in order to work.
2026-09-11 22:43:57 +02:00

504 lines
17 KiB
JavaScript

import assert from "node:assert/strict";
import zlib from "node:zlib";
import * as lib from "@clusterio/lib";
import { ModInfo, ModPack, getInstalledModUpdates, applyModRecordAdvisories } from "@clusterio/lib";
describe("lib/data/ModPack", function() {
describe("class ModPack", function() {
it("should round trip serialize", function() {
const validate = lib.compile(ModPack.jsonSchema);
function check(pack) {
const json = JSON.parse(JSON.stringify(pack));
if (!validate(json)) {
throw validate.errors;
}
assert.deepEqual(ModPack.fromJSON(json), pack);
const packStringed = ModPack.fromModPackString(pack.toModPackString());
packStringed.id = pack.id;
packStringed.exportManifest = pack.exportManifest;
assert.deepEqual(packStringed, pack);
}
check(ModPack.fromJSON({}));
check(ModPack.fromJSON({ name: "MyPack" }));
check(ModPack.fromJSON({ description: "My Description" }));
check(ModPack.fromJSON({ factorio_version: "2.0" }));
check(ModPack.fromJSON({ mods: [
{ name: "clusterio_lib", enabled: true, version: "2.0.20", sha1: "012345abcd" },
]}));
check(ModPack.fromJSON({ settings: {
"startup": {
"bool-setting": { "value": true },
},
"runtime-global": {
"number-setting": { "value": 123 },
},
"runtime-per-user": {
"string-setting": { "value": "a string" },
"color-setting": { "value": { "r": 1, "g": 1, "b": 0, "a": 1 } },
},
}}));
check(ModPack.fromJSON({ export_manifest: { assets: { setting: "settings.json" }}}));
check(ModPack.fromJSON({ deleted: true }));
check(ModPack.fromJSON({
name: "Super pack",
description: "Every option at once.",
factorio_version: "2.0",
mods: [
{ name: "clusterio_lib", enabled: true, version: "2.0.20", sha1: "012345abcd" },
],
settings: {
"startup": {
"bool-setting": { "value": true },
},
"runtime-global": {
"number-setting": { "value": 123 },
},
"runtime-per-user": {
"string-setting": { "value": "a string" },
"color-setting": { "value": { "r": 1, "g": 1, "b": 0, "a": 1 } },
},
},
export_manifest: { assets: { setting: "settings.json" }},
deleted: true,
}));
});
it("should sort integer factorio versions lexicographically", function() {
let unsortedVersions = ["1.0", "1.1.0", "0.1", "3.0.0", "1.2", "0.3.1", "0.3.3", "2.1.1", "0.0.1"];
let sortedVersions = ["0.0.1", "0.1", "0.3.1", "0.3.3", "1.0", "1.1.0", "1.2", "2.1.1", "3.0.0"];
let factorioMods = unsortedVersions.map(v => ModPack.fromJSON({ factorio_version: v }));
factorioMods.sort((a, b) => a.integerFactorioVersion - b.integerFactorioVersion);
assert.deepEqual(factorioMods.map(mod => mod.factorioVersion), sortedVersions);
});
describe(".fillDefaultSettings()", function() {
const prototypes = {
"bool-setting": {
"bool": {
name: "bool",
type: "bool-setting",
setting_type: "startup",
default_value: true,
},
},
"double-setting": {
"number": {
name: "number",
type: "double-setting",
setting_type: "runtime-global",
default_value: 123,
},
},
"string-setting": {
"string": {
name: "string",
type: "string-setting",
setting_type: "runtime-per-user",
default_value: "a string",
},
},
"color-setting": {
"color": {
name: "color",
type: "color-setting",
setting_type: "runtime-per-user",
default_value: { "r": 1, "g": 1, "b": 1, "a": 1 },
},
},
};
const mockLogger = { warn: () => {} };
it("should fill in defaults for settings", function() {
const pack = ModPack.fromJSON({});
pack.fillDefaultSettings(prototypes, mockLogger);
assert.deepEqual(pack.toJSON().settings, {
"startup": {
"bool": { "value": true },
},
"runtime-global": {
"number": { "value": 123 },
},
"runtime-per-user": {
"string": { "value": "a string" },
"color": { "value": { "r": 1, "g": 1, "b": 1, "a": 1 } },
},
});
});
it("should not overwrite existing values", function() {
const pack = ModPack.fromJSON({ settings: {
"startup": {
"bool": { "value": false },
},
"runtime-global": {
"number": { "value": 2 },
},
"runtime-per-user": {
"string": { "value": "spam" },
"color": { "value": { "r": 0, "g": 0.5, "b": 1, "a": 1 } },
},
}});
pack.fillDefaultSettings(prototypes, mockLogger);
assert.deepEqual(pack.toJSON().settings, {
"startup": {
"bool": { "value": false },
},
"runtime-global": {
"number": { "value": 2 },
},
"runtime-per-user": {
"string": { "value": "spam" },
"color": { "value": { "r": 0, "g": 0.5, "b": 1, "a": 1 } },
},
});
});
it("should ignore unknown setting_type and missing default_value", function() {
const pack = ModPack.fromJSON({});
pack.fillDefaultSettings({
"string-setting": {
"foo": {
name: "foo",
type: "string-setting",
setting_type: "magic-that-does-not-exist",
default_value: "a string",
},
"bar": {
name: "bar",
type: "string-setting",
setting_type: "startup",
},
},
}, mockLogger);
assert.deepEqual(pack.toJSON().settings, {
"startup": {},
"runtime-global": {},
"runtime-per-user": {},
});
});
it("should coerce values of the incorrect type into the correct type", function() {
const boolDefault = true;
const intDefault = 123;
const doubleDefault = 1234.5;
const stringDefault = "default";
const colorDefault = { r: 1, g: 0, b: 1, a: 1};
const settingDefaultValues = {
"bool-setting": boolDefault,
"int-setting": intDefault,
"double-setting": doubleDefault,
"string-setting": stringDefault,
"color-setting": colorDefault,
};
const possibleValues = ["bool", "int", "double", "string", "color"];
const settingPrototypes = {
"bool-setting": {},
"int-setting": {},
"double-setting": {},
"string-setting": {},
"color-setting": {},
};
for (const [type, default_value] of Object.entries(settingDefaultValues)) {
for (const value of possibleValues) {
const settingName = `${type}-with-${value}-value`;
settingPrototypes[type][settingName] = {
type,
name: settingName,
setting_type: "startup",
default_value,
};
}
};
const pack = new ModPack();
const settingValues = {
"bool": false,
"int": 1,
"double": 2.5,
"string": "str",
"color": { r: 0, g: 1, b: 0, a: 1 },
};
for (const type of Object.keys(settingDefaultValues)) {
for (const [valueType, value] of Object.entries(settingValues)) {
pack.settings.startup.set(`${type}-with-${valueType}-value`, { value });
}
}
pack.fillDefaultSettings(settingPrototypes);
assert.deepEqual(
pack.settings["startup"],
new Map([
["bool-setting-with-bool-value", false],
["bool-setting-with-int-value", boolDefault],
["bool-setting-with-double-value", boolDefault],
["bool-setting-with-string-value", boolDefault],
["bool-setting-with-color-value", boolDefault],
["int-setting-with-bool-value", intDefault],
["int-setting-with-int-value", 1],
["int-setting-with-double-value", 2.5],
["int-setting-with-string-value", intDefault],
["int-setting-with-color-value", intDefault],
["double-setting-with-bool-value", doubleDefault],
["double-setting-with-int-value", 1],
["double-setting-with-double-value", 2.5],
["double-setting-with-string-value", doubleDefault],
["double-setting-with-color-value", doubleDefault],
["string-setting-with-bool-value", "false"],
["string-setting-with-int-value", "1"],
["string-setting-with-double-value", "2.5"],
["string-setting-with-string-value", "str"],
["string-setting-with-color-value", stringDefault],
["color-setting-with-bool-value", colorDefault],
["color-setting-with-int-value", colorDefault],
["color-setting-with-double-value", colorDefault],
["color-setting-with-string-value", colorDefault],
["color-setting-with-color-value", { r: 0, g: 1, b: 0, a: 1}],
].map(([k, v]) => [k, { value: v }])),
);
});
});
describe(".fromModPackString()", function() {
it("should handle malformed strings", function() {
assert.throws(
() => ModPack.fromModPackString("AMalformedString"),
new Error("Malformed mod pack string: zlib inflate failed")
);
let badJsonMsg;
try {
JSON.parse("Not Json");
} catch (err) {
badJsonMsg = err.message;
}
assert.throws(
// eslint-disable-next-line node/no-sync
() => ModPack.fromModPackString(Buffer.from(zlib.deflateSync("Not Json")).toString("base64")),
new Error(`Malformed mod pack string: ${badJsonMsg}`)
);
assert.throws(
// eslint-disable-next-line node/no-sync
() => ModPack.fromModPackString(Buffer.from(zlib.deflateSync('{"i":1}')).toString("base64")),
new Error("Malformed mod pack string: Schema validation failed")
);
});
});
describe("toModSettingsDat()", function() {
it("should properly serialise settings", function() {
const pack = ModPack.fromJSON({ settings: {
"startup": {
"bool-setting": { "value": true },
},
"runtime-global": {
"number-setting": { "value": 123 },
},
"runtime-per-user": {
"string-setting": { "value": "a string" },
"color-setting": { "value": { "r": 1, "g": 0.5, "b": 0, "a": 1 } },
},
}});
function istr(str) {
return [
Uint8Array.from([0, str.length]),
Buffer.from(str),
];
}
/* eslint-disable @stylistic/indent */
assert.deepEqual(
pack.toModSettingsDat(),
Buffer.concat([
new Uint8Array(Uint16Array.from([1, 1, 0, 0]).buffer), // version
Uint8Array.from([0]), // reserved
Uint8Array.from([5, 0]), // dictionary
new Uint8Array(Uint32Array.from([3]).buffer), // items
...istr("startup"), // name
Uint8Array.from([5, 0]), // dictionary
new Uint8Array(Uint32Array.from([1]).buffer), // items
...istr("bool-setting"), // name
Uint8Array.from([5, 0]), // dictinary
new Uint8Array(Uint32Array.from([1]).buffer), // items
...istr("value"), // name
Uint8Array.from([1, 0, 1]), // boolean
...istr("runtime-global"), // name
Uint8Array.from([5, 0]), // dictionary
new Uint8Array(Uint32Array.from([1]).buffer), // items
...istr("number-setting"), // name
Uint8Array.from([5, 0]), // dictinary
new Uint8Array(Uint32Array.from([1]).buffer), // items
...istr("value"), // name
Uint8Array.from([2, 0]), // number
new Uint8Array(Float64Array.from([123]).buffer),
...istr("runtime-per-user"), // name
Uint8Array.from([5, 0]), // dictionary
new Uint8Array(Uint32Array.from([2]).buffer), // items
...istr("string-setting"), // name
Uint8Array.from([5, 0]), // dictinary
new Uint8Array(Uint32Array.from([1]).buffer), // items
...istr("value"), // name
Uint8Array.from([3, 0, 0, "a string".length]), // string
Buffer.from("a string"),
...istr("color-setting"), // name
Uint8Array.from([5, 0]), // dictinary
new Uint8Array(Uint32Array.from([1]).buffer), // items
...istr("value"), // name
Uint8Array.from([5, 0]), // dictinary
new Uint8Array(Uint32Array.from([4]).buffer), // items
...istr("r"), // name
Uint8Array.from([2, 0]), // number
new Uint8Array(Float64Array.from([1]).buffer),
...istr("g"), // name
Uint8Array.from([2, 0]), // number
new Uint8Array(Float64Array.from([0.5]).buffer),
...istr("b"), // name
Uint8Array.from([2, 0]), // number
new Uint8Array(Float64Array.from([0]).buffer),
...istr("a"), // name
Uint8Array.from([2, 0]), // number
new Uint8Array(Float64Array.from([1]).buffer),
])
);
});
});
describe("defaultModPacks", function() {
it("should contain at least one base game only", function() {
const modPack = ModPack.defaultModPacks.find(pack => {
const base = pack.mods.get("base");
const spaceAge = pack.mods.get("space-age");
return base && base.enabled && (!spaceAge || !spaceAge.enabled);
});
assert(modPack, "Does not contain base game only");
});
it("should contain at least one space age expansion", function() {
const modPack = ModPack.defaultModPacks.find(pack => {
const base = pack.mods.get("base");
const spaceAge = pack.mods.get("space-age");
return base && base.enabled && spaceAge && spaceAge.enabled;
});
assert(modPack, "Does not contain space age expansion");
});
});
describe("getBuiltinMods()", function() {
it("should work with versions before 2.0", function() {
const builtinMods = ModPack.getBuiltinMods("1.1");
assert.deepEqual(builtinMods, [
{ name: "base", enabled: true, version: "1.1.0" },
]);
});
it("should work with versions after 2.0", function() {
const builtinMods = ModPack.getBuiltinMods("2.0");
assert.deepEqual(builtinMods, [
{ name: "base", enabled: true, version: "2.0.0" },
{ name: "elevated-rails", enabled: false, version: "2.0.0" },
{ name: "quality", enabled: false, version: "2.0.0" },
{ name: "space-age", enabled: false, version: "2.0.0" },
]);
});
});
describe("getBuiltinModNames()", function() {
it("should work with versions before 2.0", function() {
const builtinModNames = ModPack.getBuiltinModNames("1.1");
assert.deepEqual(builtinModNames, ["base"]);
});
it("should work with versions after 2.0", function() {
const builtinModNames = ModPack.getBuiltinModNames("2.0");
assert.deepEqual(builtinModNames, ["base", "elevated-rails", "quality", "space-age"]);
});
});
});
describe("applyModRecordAdvisories()", function() {
it("finds newer installed versions for the same Factorio version", function() {
const current = ModInfo.fromJSON({
name: "source",
version: "1.0.0",
factorio_version: "2.0",
});
const newer = ModInfo.fromJSON({
name: "source",
version: "1.1.0",
factorio_version: "2.0",
});
const otherFactorioVersion = ModInfo.fromJSON({
name: "source",
version: "2.0.0",
factorio_version: "1.1",
});
const updates = getInstalledModUpdates([
{ name: "source", enabled: true, version: "1.0.0", info: current },
], [current, newer, otherFactorioVersion]);
assert.deepEqual([...updates], [["source", "1.1.0"]]);
});
it("marks compatible disabled recommended dependencies", function() {
const sourceInfo = ModInfo.fromJSON({
name: "source",
version: "1.0.0",
dependencies: ["+ quality >= 2.0.0", "+ stale >= 2.0.0", "+ active"],
});
const otherInfo = ModInfo.fromJSON({
name: "other",
version: "1.0.0",
dependencies: ["+ quality"],
});
const mods = [
{ name: "source", enabled: true, version: "1.0.0", info: sourceInfo },
{ name: "other", enabled: true, version: "1.0.0", info: otherInfo },
{ name: "ignored", enabled: false, version: "1.0.0", info: otherInfo },
{ name: "quality", enabled: false, version: "2.0.0" },
{ name: "stale", enabled: false, version: "1.0.0" },
{ name: "active", enabled: true, version: "1.0.0" },
];
const annotated = applyModRecordAdvisories(mods);
assert.deepEqual(annotated[3].advisories, [
{ type: "recommended_dependency", sourceModName: "source" },
{ type: "recommended_dependency", sourceModName: "other" },
]);
assert.equal(annotated[4].advisories, undefined);
assert.equal(annotated[5].advisories, undefined);
assert.equal(mods[3].advisories, undefined);
});
it("marks only newer resolved updates and preserves other advisories", function() {
const sourceInfo = ModInfo.fromJSON({
name: "source",
version: "1.0.0",
dependencies: ["+ quality"],
});
const mods = [
{ name: "source", enabled: true, version: "1.0.0", info: sourceInfo },
{ name: "quality", enabled: false, version: "2.0.0" },
];
const annotated = applyModRecordAdvisories(mods, new Map([
["source", "1.0.0"],
["quality", "2.1.0"],
]));
assert.deepEqual(annotated[0].advisories, undefined);
assert.deepEqual(annotated[1].advisories, [
{ type: "recommended_dependency", sourceModName: "source" },
{ type: "update_available", version: "2.1.0" },
]);
});
});
});