From 0d6beeba4761aa8035ab6b9fdfb23c423e772eee Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Wed, 9 Sep 2026 21:52:32 +0000 Subject: [PATCH 01/22] Send only recipe notification changes on inventory download Lua includes its current cleared recipe list in the download request. The instance plugin diffs it against the stored list and sends back the names to add and remove instead of the full list. Lua rebuilds the full list from the snapshot kept in the download record and runs the existing deserializer, so the end state is unchanged. Also stop the serializer from appending to the stored failed recipe list in place, which grew the list on every upload without a download in between. Co-Authored-By: Claude Fable 5.1 --- plugins/inventory_sync/README.md | 4 +- plugins/inventory_sync/instance.ts | 78 ++++++++++++++++++- plugins/inventory_sync/messages.ts | 1 + .../inventory_sync/module/inventory_sync.lua | 15 +++- plugins/inventory_sync/module/serialize.lua | 54 +++++++++++-- plugins/inventory_sync/test/instance.js | 68 ++++++++++++++++ 6 files changed, 210 insertions(+), 10 deletions(-) create mode 100644 plugins/inventory_sync/test/instance.js diff --git a/plugins/inventory_sync/README.md b/plugins/inventory_sync/README.md index c39a8283..ecf5d615 100644 --- a/plugins/inventory_sync/README.md +++ b/plugins/inventory_sync/README.md @@ -32,8 +32,8 @@ The greatly simplified data flow is as follows: 3. If the player inventory was previously uploaded but is not the same then the player inventory is deleted and the player is turned into a spectator. 4. Otherwise the player inventory is kept. -5. Scenario script asks for the player inventory from the controller. -6. Controller sends it to the scenario in a stream of chunks. +5. Scenario script asks for the player inventory from the controller, including its current recipe notification state. +6. Controller sends it to the scenario in a stream of chunks, with only the recipe notifications that differ from the current state. 7. Scenario displays and updates a progress bar as each chunk is received. 8. Once all chunks have loaded the player's synced inventory is recreated from the data and the player can start playing. diff --git a/plugins/inventory_sync/instance.ts b/plugins/inventory_sync/instance.ts index c831e8fd..39ddea17 100644 --- a/plugins/inventory_sync/instance.ts +++ b/plugins/inventory_sync/instance.ts @@ -1,3 +1,5 @@ +import util from "util"; +import zlib from "zlib"; import * as lib from "@clusterio/lib"; import { BaseInstancePlugin } from "@clusterio/host"; import { @@ -8,6 +10,11 @@ type IpcPlayerName = { player_name: string } +type IpcDownloadRequest = { + player_name: string, + recipe_notifications?: string, +} + type IpcAcquireResponse = { player_name: string, status: string, @@ -26,6 +33,49 @@ function chunkify(chunkSize: number, string: string): string[] { return string.match(new RegExp(`.{1,${chunkSize}}`, "g")) || []; } +const inflate = util.promisify(zlib.inflate); +const deflate = util.promisify(zlib.deflate); + +/** + * Decode a string produced by helpers.encode_string in Factorio + */ +async function decodeLuaString(encoded: string): Promise { + return (await inflate(Buffer.from(encoded, "base64"))).toString("utf8"); +} + +/** + * Encode a string so that helpers.decode_string in Factorio can read it + */ +async function encodeLuaString(text: string): Promise { + return (await deflate(Buffer.from(text, "utf8"))).toString("base64"); +} + +export type RecipeNotificationDelta = { + add?: string[], + remove?: string[], +} + +/** + * Compute the changes needed to turn the current cleared recipe list into the stored one + * @param stored - Cleared recipe names stored on the controller + * @param current - Cleared recipe names the instance currently has + * @returns names to add to and remove from the current list + */ +export function recipeNotificationDelta(stored: string[], current: string[]): RecipeNotificationDelta { + const storedSet = new Set(stored); + const currentSet = new Set(current); + const delta: RecipeNotificationDelta = {}; + const add = stored.filter(name => !currentSet.has(name)); + const remove = current.filter(name => !storedSet.has(name)); + if (add.length) { + delta.add = add; + } + if (remove.length) { + delta.remove = remove; + } + return delta; +} + export class InstancePlugin extends BaseInstancePlugin { playersToRelease!: Set; disconnecting!: boolean; @@ -55,7 +105,7 @@ export class InstancePlugin extends BaseInstancePlugin { ); this.instance.server.on( "ipc-inventory_sync_download", - (request: IpcPlayerName) => this.handleDownload(request).catch( + (request: IpcDownloadRequest) => this.handleDownload(request).catch( err => this.logger.error(`Error handling ipc-inventory_sync_download:\n${err.stack}`) ), ); @@ -163,7 +213,27 @@ export class InstancePlugin extends BaseInstancePlugin { ); } - async handleDownload(request: IpcPlayerName) { + /** + * Replace the stored recipe notifications with the difference from the + * current state on the instance to reduce the amount of data sent to Lua. + */ + async applyRecipeNotificationDelta(playerData: IpcPlayerData, current: string) { + if (!playerData.recipe_notifications) { + return; + } + try { + const delta = recipeNotificationDelta( + JSON.parse(await decodeLuaString(playerData.recipe_notifications)), + JSON.parse(await decodeLuaString(current)), + ); + playerData.recipe_notifications_delta = await encodeLuaString(JSON.stringify(delta)); + delete playerData.recipe_notifications; + } catch (err: any) { + this.logger.warn(`Sending full recipe notifications for ${playerData.name}:\n${err.stack}`); + } + } + + async handleDownload(request: IpcDownloadRequest) { const playerName = request.player_name; this.logger.verbose(`Downloading ${playerName}`); @@ -177,6 +247,10 @@ export class InstancePlugin extends BaseInstancePlugin { return; } + if (request.recipe_notifications) { + await this.applyRecipeNotificationDelta(response.playerData, request.recipe_notifications); + } + const chunkSize = this.instance.config.get("inventory_sync.rcon_chunk_size"); const chunks = chunkify(chunkSize, JSON.stringify(response.playerData)); this.logger.verbose(`Sending inventory for ${playerName} in ${chunks.length} chunks`); diff --git a/plugins/inventory_sync/messages.ts b/plugins/inventory_sync/messages.ts index 028c39e0..5585fe55 100644 --- a/plugins/inventory_sync/messages.ts +++ b/plugins/inventory_sync/messages.ts @@ -21,6 +21,7 @@ export type IpcPlayerData = { personal_logistic_slots?: {name:string, min:number, max:number}[], crafting_queue?: any, recipe_notifications?: string, + recipe_notifications_delta?: string, } const jsonPlayerData = Type.Object({ diff --git a/plugins/inventory_sync/module/inventory_sync.lua b/plugins/inventory_sync/module/inventory_sync.lua index bf56a57e..6df45f77 100644 --- a/plugins/inventory_sync/module/inventory_sync.lua +++ b/plugins/inventory_sync/module/inventory_sync.lua @@ -111,6 +111,13 @@ function inventory_sync.deserialize_player(player, finished_record) -- Deserialize downloaded player data local serialized_player = compat.json_to_table(finished_record.data) assert(type(serialized_player) == "table", "wrong type for serialized_player") + if serialized_player.recipe_notifications_delta then + serialized_player.recipe_notifications = serialize.apply_crafting_notification_delta( + assert(finished_record.recipe_notifications, "recipe notification delta without snapshot"), + serialized_player.recipe_notifications_delta + ) + serialized_player.recipe_notifications_delta = nil + end script_data.failed_deserialization[player.name] = serialize.deserialize_player(player, serialized_player) -- Restore player position and driving state @@ -441,8 +448,14 @@ function inventory_sync.initiate_inventory_download(player, player_record, gener local script_data = get_script_data() script_data.active_downloads[player.name] = record + -- The plugin only sends back what differs from the current notification state + record.recipe_notifications = serialize.crafting_notification_snapshot( + player, script_data.failed_deserialization[player.name] + ) + clusterio_api.send_json("inventory_sync_download", { - player_name = player.name + player_name = player.name, + recipe_notifications = record.recipe_notifications, }) -- If this is a synced player turn them into a spectator while the diff --git a/plugins/inventory_sync/module/serialize.lua b/plugins/inventory_sync/module/serialize.lua index 6d62a6aa..78430de3 100644 --- a/plugins/inventory_sync/module/serialize.lua +++ b/plugins/inventory_sync/module/serialize.lua @@ -533,17 +533,61 @@ function serialize.serialize_crafting_notifications(player, failed) recipes[recipe.name] = nil end - -- Convert to a list of names - local recipe_names = failed or {} -- Includes previous recipes if any - local index = #recipe_names + 1 + -- Convert to a list of names, including previously failed recipes if any + local recipe_names = {} + for _, name in ipairs(failed or {}) do + recipe_names[#recipe_names + 1] = name + end for name in pairs(recipes) do - recipe_names[index] = name - index = index + 1 + recipe_names[#recipe_names + 1] = name end return helpers.encode_string(helpers.table_to_json(recipe_names)) end +--- Current notification state, sent with download requests so the instance plugin only returns the difference +--- @param player LuaPlayer +--- @param failed FailedDeserializationPlayerData? +--- @return string? +function serialize.crafting_notification_snapshot(player, failed) + if not recipe_notifications_api then + return nil + end + return serialize.serialize_crafting_notifications(player, failed and failed.recipe_notifications) +end + +--- @class CraftingNotificationDelta +--- @field add string[]? Recipe names to add to the cleared list +--- @field remove string[]? Recipe names to remove from the cleared list + +--- Rebuild the full serialized form from the snapshot the delta was computed against +--- @param snapshot string +--- @param serialized_delta string +--- @return string? +function serialize.apply_crafting_notification_delta(snapshot, serialized_delta) + local recipe_names = helpers.json_to_table(assert(helpers.decode_string(snapshot))) + assert(type(recipe_names) == "table", "wrong type decoded from json_to_table") + --- @type CraftingNotificationDelta + local delta = helpers.json_to_table(assert(helpers.decode_string(serialized_delta))) + assert(type(delta) == "table", "wrong type decoded from json_to_table") + local cleared = {} + for _, name in pairs(recipe_names) do + cleared[name] = true + end + for _, name in pairs(delta.remove or {}) do + cleared[name] = nil + end + for _, name in pairs(delta.add or {}) do + cleared[name] = true + end + + local merged = {} + for name in pairs(cleared) do + merged[#merged + 1] = name + end + return helpers.encode_string(helpers.table_to_json(merged)) +end + --- @param player LuaPlayer --- @param serialized string --- @return string[]? failed Recipe names that do not exist diff --git a/plugins/inventory_sync/test/instance.js b/plugins/inventory_sync/test/instance.js new file mode 100644 index 00000000..3354142a --- /dev/null +++ b/plugins/inventory_sync/test/instance.js @@ -0,0 +1,68 @@ +"use strict"; +const assert = require("assert").strict; +const util = require("util"); +const zlib = require("zlib"); + +const { recipeNotificationDelta, InstancePlugin } = require("../dist/node/instance"); + +const inflate = util.promisify(zlib.inflate); +const deflate = util.promisify(zlib.deflate); + +// Matches helpers.encode_string in Factorio +async function encode(value) { + return (await deflate(Buffer.from(JSON.stringify(value)))).toString("base64"); +} +async function decode(encoded) { + return JSON.parse((await inflate(Buffer.from(encoded, "base64"))).toString()); +} + +describe("inventory_sync", function() { + describe("recipeNotificationDelta()", function() { + it("should be empty when the lists match", function() { + assert.deepEqual(recipeNotificationDelta(["a", "b"], ["b", "a"]), {}); + }); + it("should add stored names missing from current", function() { + assert.deepEqual(recipeNotificationDelta(["a", "b", "c"], ["a"]), { add: ["b", "c"] }); + }); + it("should remove current names missing from stored", function() { + assert.deepEqual(recipeNotificationDelta(["a"], ["a", "b"]), { remove: ["b"] }); + }); + it("should handle both directions at once", function() { + assert.deepEqual(recipeNotificationDelta(["a", "b"], ["b", "c"]), { add: ["a"], remove: ["c"] }); + }); + it("should keep duplicates in stored out of the current list", function() { + assert.deepEqual(recipeNotificationDelta(["a", "a"], ["a"]), {}); + }); + }); + + describe("InstancePlugin.applyRecipeNotificationDelta()", function() { + const warnings = []; + const plugin = { logger: { warn: msg => warnings.push(msg) } }; + const apply = InstancePlugin.prototype.applyRecipeNotificationDelta.bind(plugin); + + it("should replace the stored list with an encoded delta", async function() { + const playerData = { name: "test", recipe_notifications: await encode(["a", "b"]) }; + await apply(playerData, await encode(["b", "c"])); + assert.equal(playerData.recipe_notifications, undefined); + assert.deepEqual(await decode(playerData.recipe_notifications_delta), { add: ["a"], remove: ["c"] }); + }); + it("should send an empty delta when nothing changed", async function() { + const playerData = { name: "test", recipe_notifications: await encode(["a"]) }; + await apply(playerData, await encode(["a"])); + assert.deepEqual(await decode(playerData.recipe_notifications_delta), {}); + }); + it("should do nothing without stored notifications", async function() { + const playerData = { name: "test" }; + await apply(playerData, await encode(["a"])); + assert.deepEqual(playerData, { name: "test" }); + }); + it("should fall back to the full list on invalid input", async function() { + const stored = await encode(["a"]); + const playerData = { name: "test", recipe_notifications: stored }; + await apply(playerData, "not base64 zlib"); + assert.equal(playerData.recipe_notifications, stored); + assert.equal(playerData.recipe_notifications_delta, undefined); + assert.equal(warnings.length, 1); + }); + }); +}); From 1272c0e8e4b3a910fdcea8110dbf8c3ada493e43 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Wed, 9 Sep 2026 22:18:24 +0000 Subject: [PATCH 02/22] Test the inventory sync download handler Covers the empty response, the full list without a snapshot, the delta with a snapshot, and the IPC registration. Co-Authored-By: Claude Fable 5.1 --- plugins/inventory_sync/test/instance.js | 62 +++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/plugins/inventory_sync/test/instance.js b/plugins/inventory_sync/test/instance.js index 3354142a..e292c19f 100644 --- a/plugins/inventory_sync/test/instance.js +++ b/plugins/inventory_sync/test/instance.js @@ -3,7 +3,12 @@ const assert = require("assert").strict; const util = require("util"); const zlib = require("zlib"); +const lib = require("@clusterio/lib"); + +const mock = require("../../../test/mock"); const { recipeNotificationDelta, InstancePlugin } = require("../dist/node/instance"); +const { DownloadResponse } = require("../dist/node/messages"); +const info = require("../dist/node/index").plugin; const inflate = util.promisify(zlib.inflate); const deflate = util.promisify(zlib.deflate); @@ -65,4 +70,61 @@ describe("inventory_sync", function() { assert.equal(warnings.length, 1); }); }); + + describe("InstancePlugin.handleDownload()", function() { + let instancePlugin; + before(async function() { + try { + lib.registerPluginMessages([info]); + } catch (err) { + // Already registered by the full test suite + } + instancePlugin = await mock.createInstancePlugin(InstancePlugin, info); + instancePlugin.instance.mockConfigEntries.set("inventory_sync.rcon_chunk_size", 100000); + }); + beforeEach(function() { + instancePlugin.instance.server.reset(); + }); + + function respondWith(playerData) { + instancePlugin.instance.connector.once("send", message => { + instancePlugin.instance.connector.emit("message", new lib.MessageResponse( + 1, message.dst, message.src, new DownloadResponse(playerData) + )); + }); + } + function downloadCommand(playerData) { + const json = lib.escapeString(JSON.stringify(playerData)); + return `/sc inventory_sync.download_inventory('test','${json}',1,1)`; + } + + it("should send an empty download when the controller has no data", async function() { + respondWith(undefined); + await instancePlugin.handleDownload({ player_name: "test" }); + assert.deepEqual( + instancePlugin.instance.server.rconCommands, + ["/sc inventory_sync.download_inventory('test',nil,0,0)"] + ); + }); + it("should send the full list when no snapshot was given", async function() { + const playerData = { generation: 1, name: "test", recipe_notifications: await encode(["a"]) }; + respondWith({ ...playerData }); + await instancePlugin.handleDownload({ player_name: "test" }); + assert.deepEqual(instancePlugin.instance.server.rconCommands, [downloadCommand(playerData)]); + }); + it("should send a delta when a snapshot was given", async function() { + respondWith({ generation: 1, name: "test", recipe_notifications: await encode(["a", "b"]) }); + await instancePlugin.handleDownload({ player_name: "test", recipe_notifications: await encode(["b"]) }); + const expected = { generation: 1, name: "test", recipe_notifications_delta: await encode({ add: ["a"] }) }; + assert.deepEqual(instancePlugin.instance.server.rconCommands, [downloadCommand(expected)]); + }); + it("should be invoked by the download ipc", async function() { + respondWith(undefined); + instancePlugin.instance.server.emit("ipc-inventory_sync_download", { player_name: "test" }); + for (let i = 0; i < 100 && !instancePlugin.instance.server.rconCommands.length; i++) { + await new Promise(resolve => setImmediate(resolve)); + } + assert.equal(instancePlugin.instance.server.rconCommands.length, 1); + }); + }); }); From 7ff2092547722537d52effd8c7ae09b6b8ca625d Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Wed, 9 Sep 2026 22:40:32 +0000 Subject: [PATCH 03/22] Add update all for the controller, hosts and plugins Updating a single plugin can leave the installation with multiple versions of @clusterio/lib. Passing the core package and every npm installed plugin to a single npm update call lets npm resolve them together. UpdateAllRequest targets the controller or a host, requires both the core update permission and core.plugin.update, and is gated by both allow_remote_updates and allow_plugin_updates. The web UI Updates modal gets an Update All action and ctl gets --all on the update commands. Closes #910 Co-Authored-By: Claude Fable 5.1 --- docs/managing-a-cluster.md | 7 +++ packages/controller/src/ControlConnection.ts | 11 ++++ packages/ctl/src/commands_controller.ts | 9 +++- packages/ctl/src/commands_host.ts | 9 +++- packages/host/src/Host.ts | 11 ++++ packages/lib/src/data/messages_plugin.ts | 13 +++++ packages/lib/src/link/messages.ts | 1 + packages/lib/src/rce_ops.ts | 11 +++- packages/web_ui/src/components/NpmButton.tsx | 24 ++++++++- test/lib/rce_ops.js | 14 +++++ test/messages/plugin.test.js | 55 ++++++++++++++++++++ 11 files changed, 158 insertions(+), 7 deletions(-) diff --git a/docs/managing-a-cluster.md b/docs/managing-a-cluster.md index 476d1314..aa3e8587 100644 --- a/docs/managing-a-cluster.md +++ b/docs/managing-a-cluster.md @@ -247,6 +247,13 @@ This command lists all plugins currently known to the remote. Newly installed pl If remote plugin updates are enabled on the remote, this will execute `npm update --save `. Changes will not be applied until the remote is restarted. If the `--restart` flag is provided, the remote will automatically restart once the update is complete. +### Update All + + ctl> controller update --all [--restart] + ctl> host update --all [--restart] + +Updates the Clusterio package and all plugins installed from npm in a single `npm update --save` command. Updating everything at once avoids ending up with multiple versions of `@clusterio/lib` in the installation. Requires both remote updates and remote plugin updates to be enabled on the remote. + ### Install ctl> controller plugin install [--restart] diff --git a/packages/controller/src/ControlConnection.ts b/packages/controller/src/ControlConnection.ts index c9c9f8b0..865c3ba9 100644 --- a/packages/controller/src/ControlConnection.ts +++ b/packages/controller/src/ControlConnection.ts @@ -128,6 +128,7 @@ export default class ControlConnection extends BaseConnection { this.handle(lib.PluginListRequest, this.handlePluginListRequest.bind(this)); this.handle(lib.PluginUpdateRequest, this.handlePluginUpdateRequest.bind(this)); this.handle(lib.PluginInstallRequest, this.handlePluginInstallRequest.bind(this)); + this.handle(lib.UpdateAllRequest, this.handleUpdateAllRequest.bind(this)); this.handle(lib.DebugDumpWsRequest, this.handleDebugDumpWsRequest.bind(this)); } @@ -1307,6 +1308,16 @@ export default class ControlConnection extends BaseConnection { return await lib.handlePluginInstall(request.pluginPackage); } + async handleUpdateAllRequest(request: lib.UpdateAllRequest) { + if (!this._controller.config.get("controller.allow_remote_updates")) { + throw new lib.RequestError("Remote updates are disabled on this machine"); + } + if (!this._controller.config.get("controller.allow_plugin_updates")) { + throw new lib.RequestError("Plugin updates are disabled on this machine"); + } + return await lib.handleUpdateAll("@clusterio/controller", this._controller.pluginInfos); + } + async handlePluginListRequest(request: lib.PluginListRequest) { return this._controller.pluginInfos.map(pluginInfo => lib.PluginDetails.fromNodeEnvInfo( pluginInfo, diff --git a/packages/ctl/src/commands_controller.ts b/packages/ctl/src/commands_controller.ts index fbdde7a4..c7e73ba3 100644 --- a/packages/ctl/src/commands_controller.ts +++ b/packages/ctl/src/commands_controller.ts @@ -28,9 +28,14 @@ controllerCommands.add(new lib.Command({ controllerCommands.add(new lib.Command({ definition: ["update", "Update the controller", (yargs) => { yargs.option("restart", { alias: "r", type: "boolean", description: "Restart after update" }); + yargs.option("all", { alias: "a", type: "boolean", description: "Also update all plugins" }); }], - handler: async function(args: { restart: boolean }, control: Control) { - await control.send(new lib.ControllerUpdateRequest()); + handler: async function(args: { restart: boolean, all: boolean }, control: Control) { + if (args.all) { + await control.send(new lib.UpdateAllRequest()); + } else { + await control.send(new lib.ControllerUpdateRequest()); + } if (args.restart) { await control.send(new lib.ControllerRestartRequest()); } else { diff --git a/packages/ctl/src/commands_host.ts b/packages/ctl/src/commands_host.ts index c44db2fe..82240ae9 100644 --- a/packages/ctl/src/commands_host.ts +++ b/packages/ctl/src/commands_host.ts @@ -101,10 +101,15 @@ hostCommands.add(new lib.Command({ definition: ["update ", "Update the host", (yargs) => { yargs.positional("host", { describe: "Host to update", type: "string" }); yargs.option("restart", { alias: "r", type: "boolean", description: "Restart after update" }); + yargs.option("all", { alias: "a", type: "boolean", description: "Also update all plugins" }); }], - handler: async function(args: { host: string, restart: boolean }, control: Control) { + handler: async function(args: { host: string, restart: boolean, all: boolean }, control: Control) { let hostId = await lib.resolveHost(control, args.host); - await control.sendTo({ hostId }, new lib.HostUpdateRequest()); + if (args.all) { + await control.sendTo({ hostId }, new lib.UpdateAllRequest()); + } else { + await control.sendTo({ hostId }, new lib.HostUpdateRequest()); + } if (args.restart) { await control.sendTo({ hostId }, new lib.HostRestartRequest()); } else { diff --git a/packages/host/src/Host.ts b/packages/host/src/Host.ts index c97ca6fc..80003dd0 100644 --- a/packages/host/src/Host.ts +++ b/packages/host/src/Host.ts @@ -403,6 +403,7 @@ export default class Host extends lib.Link { this.handle(lib.PluginListRequest, this.handlePluginListRequest.bind(this)); this.handle(lib.PluginUpdateRequest, this.handlePluginUpdateRequest.bind(this)); this.handle(lib.PluginInstallRequest, this.handlePluginInstallRequest.bind(this)); + this.handle(lib.UpdateAllRequest, this.handleUpdateAllRequest.bind(this)); this.snoopEvent(lib.InstanceAdminlistUpdateEvent, this.handleAdminlistUpdateEvent.bind(this)); this.snoopEvent(lib.InstanceBanlistUpdateEvent, this.handleBanlistUpdateEvent.bind(this)); @@ -1151,6 +1152,16 @@ export default class Host extends lib.Link { return await lib.handlePluginInstall(request.pluginPackage); } + async handleUpdateAllRequest(request: lib.UpdateAllRequest) { + if (!this.config.get("host.allow_remote_updates")) { + throw new lib.RequestError("Remote updates are disabled on this machine"); + } + if (!this.config.get("host.allow_plugin_updates")) { + throw new lib.RequestError("Plugin updates are disabled on this machine"); + } + return await lib.handleUpdateAll("@clusterio/host", this.pluginInfos); + } + async handlePluginListRequest(request: lib.PluginListRequest) { return this.pluginInfos.map(pluginInfo => lib.PluginDetails.fromNodeEnvInfo( pluginInfo, diff --git a/packages/lib/src/data/messages_plugin.ts b/packages/lib/src/data/messages_plugin.ts index edbe7664..e2c9f02c 100644 --- a/packages/lib/src/data/messages_plugin.ts +++ b/packages/lib/src/data/messages_plugin.ts @@ -1,6 +1,8 @@ import { Type, Static } from "@sinclair/typebox"; import { jsonArray } from "./composites"; import { PluginNodeEnvInfo } from "../plugin"; +import { Address, MessageRequest } from "./messages_core"; +import type { IUser } from "./UserDetails"; /* This is similar to other plugin definitions but with lots removed */ export class PluginDetails { @@ -91,3 +93,14 @@ export class PluginInstallRequest { return this.pluginPackage; } } + +export class UpdateAllRequest { + declare ["constructor"]: typeof UpdateAllRequest; + static type = "request" as const; + static src = "control" as const; + static dst = ["controller", "host"] as const; + static permission(user: IUser, message: MessageRequest) { + user.checkPermission(message.dst.type === Address.host ? "core.host.update" : "core.controller.update"); + user.checkPermission("core.plugin.update"); + } +} diff --git a/packages/lib/src/link/messages.ts b/packages/lib/src/link/messages.ts index 1db48338..cb2aaee4 100644 --- a/packages/lib/src/link/messages.ts +++ b/packages/lib/src/link/messages.ts @@ -140,4 +140,5 @@ export const dataClasses: (RequestClass | EventClass) plugin.PluginListRequest, plugin.PluginUpdateRequest, plugin.PluginInstallRequest, + plugin.UpdateAllRequest, ]; diff --git a/packages/lib/src/rce_ops.ts b/packages/lib/src/rce_ops.ts index b5ad5be5..759312a4 100644 --- a/packages/lib/src/rce_ops.ts +++ b/packages/lib/src/rce_ops.ts @@ -19,8 +19,8 @@ async function logExec(cmd: string) { } } -export async function updatePackage(name: string) { - return logExec(`npm update --save ${name}`); +export async function updatePackage(...names: string[]) { + return logExec(`npm update --save ${names.join(" ")}`); } export async function installPackage(name: string) { @@ -35,6 +35,13 @@ export async function handlePluginUpdate(pluginName: string, pluginInfos: Plugin return await updatePackage(pluginName); } +export async function handleUpdateAll(corePackage: string, pluginInfos: PluginNodeEnvInfo[]) { + const pluginPackages = pluginInfos + .map(plugin => plugin.npmPackage) + .filter((name): name is string => name !== undefined); + return await updatePackage(corePackage, ...pluginPackages); +} + export async function handlePluginInstall(pluginName: string) { if (pluginName.length > 214 || /[^a-zA-Z0-9\-_.+@\/]/.test(pluginName)) { // https://docs.npmjs.com/cli/v11/configuring-npm/package-json#name diff --git a/packages/web_ui/src/components/NpmButton.tsx b/packages/web_ui/src/components/NpmButton.tsx index 1a5a83e2..17f1883c 100644 --- a/packages/web_ui/src/components/NpmButton.tsx +++ b/packages/web_ui/src/components/NpmButton.tsx @@ -27,6 +27,17 @@ function NpmRemoteUpdate({ setApplyAction, form, target }: NpmActionProps) { ; } +function NpmUpdateAll({ setApplyAction, form, target }: NpmActionProps) { + const control = useContext(ControlContext); + + setApplyAction(async () => { + await control.sendTo(target, new lib.UpdateAllRequest()); + }); + + return <> + ; +} + function NpmPluginUpdate({ setApplyAction, form, target }: NpmActionProps) { const control = useContext(ControlContext); const [plugins, setPlugins] = useState([]); @@ -128,6 +139,8 @@ export function NpmButton(props: { } }, [hostId]); + const coreUpdatePermission = `core.${props.target === "controller" ? "controller" : "host"}.update`; + function onValuesChange({ action } : { action?: string }) { if (action) { setFormAction(action); @@ -175,10 +188,17 @@ export function NpmButton(props: {
- {account.hasPermission(`core.${props.target === "controller" ? "controller" : "host"}.update`) + {account.hasPermission(coreUpdatePermission) ? Update Clusterio : undefined} + {account.hasPermission(coreUpdatePermission) && account.hasPermission("core.plugin.update") + ? + Update All + : undefined} {account.hasPermission("core.plugin.update") ? Update Plugin @@ -198,6 +218,8 @@ export function NpmButton(props: { } {formAction === "remote_update" ? : undefined} + {formAction === "update_all" + ? : undefined} {formAction === "plugin_update" ? : undefined} {formAction === "plugin_install" diff --git a/test/lib/rce_ops.js b/test/lib/rce_ops.js index c5b272b7..4f503ad4 100644 --- a/test/lib/rce_ops.js +++ b/test/lib/rce_ops.js @@ -29,6 +29,20 @@ describe("rce_ops", function() { }); }); + describe("handleUpdateAll", function() { + it("updates the core package and all npm plugins in one command", async function() { + const _audit = lib.logger.audit; + const messages = []; + lib.logger.audit = (msg) => { messages.push(msg); }; + try { + await lib.handleUpdateAll("core", [{ npmPackage: "foo" }, { name: "local" }, { npmPackage: "bar" }]); + } finally { + lib.logger.audit = _audit; + } + assert.deepEqual(messages, ["RCE | npm update --save core foo bar"]); + }); + }); + describe("handlePluginInstall", function() { const _fetch = global.fetch; before(function() { diff --git a/test/messages/plugin.test.js b/test/messages/plugin.test.js index e2c7d9d8..ef2ba9c2 100644 --- a/test/messages/plugin.test.js +++ b/test/messages/plugin.test.js @@ -134,6 +134,61 @@ describe("messages/plugin", function() { }); }); + describe("UpdateAllRequest", function() { + function checkedPermissions(dst) { + const permissions = []; + const user = { checkPermission(permission) { permissions.push(permission); } }; + lib.UpdateAllRequest.permission(user, { dst: lib.Address.fromShorthand(dst) }); + return permissions; + } + it("requires controller and plugin update permissions on the controller", function() { + assert.deepEqual(checkedPermissions("controller"), ["core.controller.update", "core.plugin.update"]); + }); + it("requires host and plugin update permissions on a host", function() { + assert.deepEqual(checkedPermissions({ hostId: 1 }), ["core.host.update", "core.plugin.update"]); + }); + it("runs on the controller", async function() { + controller.pluginInfos = [{ npmPackage: "foo" }]; + controller.config.set("controller.allow_remote_updates", true); + controller.config.set("controller.allow_plugin_updates", true); + await controlConnection.handleUpdateAllRequest(new lib.UpdateAllRequest()); + }); + it("rejects if remote updates are disabled on the controller", async function() { + controller.config.set("controller.allow_remote_updates", false); + await assert.rejects( + controlConnection.handleUpdateAllRequest(new lib.UpdateAllRequest()), + /Remote updates are disabled on this machine/ + ); + }); + it("rejects if plugin updates are disabled on the controller", async function() { + controller.config.set("controller.allow_plugin_updates", false); + await assert.rejects( + controlConnection.handleUpdateAllRequest(new lib.UpdateAllRequest()), + /Plugin updates are disabled on this machine/ + ); + }); + it("runs on a host", async function() { + host.pluginInfos = [{ npmPackage: "foo" }]; + host.config.set("host.allow_remote_updates", true); + host.config.set("host.allow_plugin_updates", true); + await host.handleUpdateAllRequest(new lib.UpdateAllRequest()); + }); + it("rejects if remote updates are disabled on the host", async function() { + host.config.set("host.allow_remote_updates", false); + await assert.rejects( + host.handleUpdateAllRequest(new lib.UpdateAllRequest()), + /Remote updates are disabled on this machine/ + ); + }); + it("rejects if plugin updates are disabled on the host", async function() { + host.config.set("host.allow_plugin_updates", false); + await assert.rejects( + host.handleUpdateAllRequest(new lib.UpdateAllRequest()), + /Plugin updates are disabled on this machine/ + ); + }); + }); + describe("PluginInstallRequest", function() { const _fetch = global.fetch; before(function() { From 637ec0f901d83ee59401a29237e3883331741bbf Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Wed, 9 Sep 2026 22:51:13 +0000 Subject: [PATCH 04/22] Cover the --all flag on the ctl update commands Co-Authored-By: Claude Fable 5.1 --- test/integration/clusterio.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js index 26d8dfa9..57fe98b7 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -522,6 +522,9 @@ describe("Integration of Clusterio", function() { it("runs", async function() { await execCtl("controller update"); }); + it("accepts --all", async function() { + await execCtl("controller update --all"); + }); it("accepts --restart", async function() { // We cannot restart the controller, so we check for controller error instead await assert.rejects( @@ -604,6 +607,9 @@ describe("Integration of Clusterio", function() { it("runs", async function() { await execCtl("host update 4"); }); + it("accepts --all", async function() { + await execCtl("host update 4 --all"); + }); it("accepts --restart", async function() { // We cannot restart the host, so we check for host error instead await assert.rejects( From d6dce12acccb9a5f010f1e7abbc133ad6d33da33 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Thu, 10 Sep 2026 19:35:04 +0200 Subject: [PATCH 05/22] Use factory pattern for yargs The ESM import of yargs does not support the singleton way of using yargs, instead we have to call the yargs function to get a parser that can be augmented with the options. The .argv property has also been deprecated. Replace it with a call to .parse() instead. --- packages/controller/controller.ts | 4 ++-- packages/create/create.js | 6 +++--- packages/host/host.ts | 2 +- packages/lib/build_mod.js | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/controller/controller.ts b/packages/controller/controller.ts index 44d92502..9787bab6 100644 --- a/packages/controller/controller.ts +++ b/packages/controller/controller.ts @@ -268,7 +268,7 @@ interface InitializeParameters { async function initialize(): Promise { // argument parsing - const args = await yargs + const args = await yargs(process.argv.slice(2)) .scriptName("controller") .usage("$0 [options]") .option("log-level", { @@ -340,7 +340,7 @@ async function initialize(): Promise { }) .demandCommand(1, "You need to specify a command to run") .strict() - .argv + .parse() ; // Combined log stream of the whole cluster. diff --git a/packages/create/create.js b/packages/create/create.js index 3d0557af..1fc26b6f 100755 --- a/packages/create/create.js +++ b/packages/create/create.js @@ -750,7 +750,7 @@ async function inquirerMissingArgs(args) { } async function main() { - let args = yargs + let yarg = yargs(process.argv.slice(2)) .option("log-level", { nargs: 1, describe: "Log level to print to stdout", default: "info", choices: ["none"].concat(Object.keys(levels)), type: "string", @@ -805,14 +805,14 @@ async function main() { ; if (process.platform === "linux") { - args = args + yarg = yarg .option("allow-install-as-root", { nargs: 0, describe: "(Linux only) Allow installing as root (not recommended)", type: "boolean", }) ; } - args = args.argv; + const args = yarg.parse(); setLogLevel(args.logLevel === "none" ? -1 : levels[args.logLevel]); dev = args.dev; diff --git a/packages/host/host.ts b/packages/host/host.ts index a2b882fb..91aeace5 100755 --- a/packages/host/host.ts +++ b/packages/host/host.ts @@ -82,7 +82,7 @@ export class HostConnector extends lib.WebSocketClientConnector { async function startHost() { // argument parsing // eslint-disable-next-line node/no-sync - const args = yargs + const args = yargs(process.argv.slice(2)) .scriptName("host") .usage("$0 [options]") .option("log-level", { diff --git a/packages/lib/build_mod.js b/packages/lib/build_mod.js index 1406eeda..d89c5dfb 100644 --- a/packages/lib/build_mod.js +++ b/packages/lib/build_mod.js @@ -121,7 +121,7 @@ async function build(args) { async function main() { - const args = yargs + const args = yargs(process.argv.slice(2)) .scriptName("build") .options({ "clean": { describe: "Remove previous builds", type: "boolean", default: false }, @@ -144,7 +144,7 @@ async function main() { }, }) .strict() - .argv + .parse() ; await build(args); From 30083b96c117850eb70b41d993b3a6da9693c902 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Thu, 10 Sep 2026 19:55:15 +0200 Subject: [PATCH 06/22] Add packagePath to plugin info Provide the absolute path to the package.json file in the resolved plugin info to simplify the logic around loading files from it. --- packages/controller/src/Controller.ts | 3 +-- packages/host/src/Host.ts | 3 +-- packages/host/src/patch.ts | 3 +-- packages/lib/src/plugin.ts | 4 ++++ packages/lib/src/plugin_loader.ts | 20 ++++++++++---------- test/controller/Controller.js | 2 +- test/host.js | 2 +- test/lib/plugin_loader.js | 1 + 8 files changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/controller/src/Controller.ts b/packages/controller/src/Controller.ts index bcbdd007..f24469df 100644 --- a/packages/controller/src/Controller.ts +++ b/packages/controller/src/Controller.ts @@ -644,8 +644,7 @@ export default class Controller { } packageName = pluginInfo.npmPackage ?? pluginInfo.name; - const pluginPackageJsonPath = require.resolve(path.posix.join(pluginInfo.requirePath, "package.json")); - const pluginPackageJson = JSON.parse(await fs.readFile(pluginPackageJsonPath, "utf8")); + const pluginPackageJson = JSON.parse(await fs.readFile(pluginInfo.packagePath, "utf8")); if (pluginInfo.version !== pluginPackageJson.version) { this.config.restartRequired = true; return true; diff --git a/packages/host/src/Host.ts b/packages/host/src/Host.ts index c97ca6fc..7cae0ce6 100644 --- a/packages/host/src/Host.ts +++ b/packages/host/src/Host.ts @@ -876,8 +876,7 @@ export default class Host extends lib.Link { } packageName = pluginInfo.npmPackage ?? pluginInfo.name; - const pluginPackageJsonPath = require.resolve(path.posix.join(pluginInfo.requirePath, "package.json")); - const pluginPackageJson = JSON.parse(await fs.readFile(pluginPackageJsonPath, "utf8")); + const pluginPackageJson = JSON.parse(await fs.readFile(pluginInfo.packagePath, "utf8")); if (pluginInfo.version !== pluginPackageJson.version) { this.config.restartRequired = true; return true; diff --git a/packages/host/src/patch.ts b/packages/host/src/patch.ts index 639ce65f..c94f5ce1 100644 --- a/packages/host/src/patch.ts +++ b/packages/host/src/patch.ts @@ -91,8 +91,7 @@ export class SaveModule { } static async fromPlugin(plugin: BaseInstancePlugin) { - let pluginPackagePath = require.resolve(path.posix.join(plugin.info.requirePath, "package.json")); - let moduleDirectory = path.join(path.dirname(pluginPackagePath), "module"); + let moduleDirectory = path.join(path.dirname(plugin.info.packagePath), "module"); try { await fs.access(moduleDirectory); } catch (err: any) { diff --git a/packages/lib/src/plugin.ts b/packages/lib/src/plugin.ts index 94942f07..456cde2a 100644 --- a/packages/lib/src/plugin.ts +++ b/packages/lib/src/plugin.ts @@ -46,6 +46,10 @@ export type PluginNodeEnvInfo = PluginDeclaration & { * server in order for the web interface to be able to load the plugin. */ webStaticPath: string; + /** + * Absolute path to the package.json file for the plugin. + */ + packagePath: string; requirePath: string; version: string; manifest: any; diff --git a/packages/lib/src/plugin_loader.ts b/packages/lib/src/plugin_loader.ts index 90a1fc72..31ec7a03 100644 --- a/packages/lib/src/plugin_loader.ts +++ b/packages/lib/src/plugin_loader.ts @@ -26,11 +26,15 @@ export async function loadPluginInfos(pluginList: Map) { for (let [pluginName, pluginPath] of pluginList) { let pluginInfo: libPlugin.PluginNodeEnvInfo; let pluginPackage: { name?: string, version: string, main?: string, private?: boolean }; + let packagePath; - // Check if plugin path exists, otherwise remove it + // Check if plugin has a package.json file, otherwise remove it try { - require.resolve(pluginPath); - } catch { + const absolute = path.isAbsolute(pluginPath); + const packageImport = path.posix.join(pluginPath, "package.json"); + packagePath = require.resolve(packageImport); + await fs.access(packagePath, fs.constants.F_OK); + } catch (err) { let errMsg = `Plugin path ${pluginPath} does not exist`; try { await fs.access(pluginPath, fs.constants.F_OK); @@ -43,7 +47,7 @@ export async function loadPluginInfos(pluginList: Map) { try { pluginInfo = require(pluginPath).plugin; - pluginPackage = require(path.posix.join(pluginPath, "package.json")); + pluginPackage = require(packagePath); } catch (err: any) { if (err.code === "InstallationError") { throw err; @@ -69,12 +73,8 @@ export async function loadPluginInfos(pluginList: Map) { continue; } - pluginInfo.webStaticPath = path.join( - path.dirname( - require.resolve(path.posix.join(pluginPath, "package.json")) - ), - "dist", "web", "static", - ); + pluginInfo.webStaticPath = path.join(path.dirname(packagePath), "dist", "web", "static"); + pluginInfo.packagePath = packagePath; pluginInfo.requirePath = pluginPath; pluginInfo.version = pluginPackage.version; pluginInfo.npmPackage = !pluginPackage.private && pluginPath === pluginPackage.name ? pluginPath : undefined; diff --git a/test/controller/Controller.js b/test/controller/Controller.js index 35e6db9c..736d4cbe 100644 --- a/test/controller/Controller.js +++ b/test/controller/Controller.js @@ -241,7 +241,7 @@ describe("controller/src/Controller", function() { controller.config.set("global_chat.load_plugin", true); controller.pluginInfos[0] = { name: "global_chat", - requirePath: path.dirname(require.resolve("@clusterio/controller/package.json")), + packagePath: require.resolve("@clusterio/controller/package.json"), version: controllerVersion, }; }); diff --git a/test/host.js b/test/host.js index 4308c189..ad97bd83 100644 --- a/test/host.js +++ b/test/host.js @@ -123,7 +123,7 @@ describe("Host testing", function() { // This can be used more generally, but i did not want to interfere with handleSyncUserListsEvent const pluginInfos = [{ name: "global_chat", - requirePath: path.dirname(require.resolve("@clusterio/host/package.json")), + packagePath: require.resolve("@clusterio/host/package.json"), version: hostVersion, }]; const hostConfig = new lib.HostConfig("host", { "host.version": hostVersion }); diff --git a/test/lib/plugin_loader.js b/test/lib/plugin_loader.js index 02476e96..58281988 100644 --- a/test/lib/plugin_loader.js +++ b/test/lib/plugin_loader.js @@ -47,6 +47,7 @@ describe("lib/plugin_loader", function() { name: "test", version: "0.0.1", npmPackage: undefined, + packagePath: path.resolve(path.join(testPlugin, "package.json")), requirePath: path.resolve(testPlugin), webStaticPath: path.resolve(path.join(testPlugin, "dist", "web", "static")), }] From 511b8b7d975fd02b7a5500a246877c34b4a49ff7 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Thu, 10 Sep 2026 20:01:54 +0200 Subject: [PATCH 07/22] Remove global_chat dependency in restart test Use a fictional restart_test mock plugin added as a before step instead of global_chat for the .checkRestartRequired() test. Prevents the test from failing if the integration tests are not also run. Also fixes the fixture not accounting for setting load_plugin causing restartRequired to be set to true due to the integration tests having already set it to the same value. --- test/controller/Controller.js | 12 ++++++++---- test/host.js | 7 +++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/test/controller/Controller.js b/test/controller/Controller.js index 736d4cbe..6054854a 100644 --- a/test/controller/Controller.js +++ b/test/controller/Controller.js @@ -10,6 +10,7 @@ const express = require("express"); const { ControllerConfig, Address, RequestError, InstanceConfig, SystemInfo, Role, ModPack, + addPluginConfigFields, } = require("@clusterio/lib"); class MockEvent {} @@ -234,13 +235,16 @@ describe("controller/src/Controller", function() { }); }); describe(".checkRestartRequired()", function() { + before(function() { + addPluginConfigFields([{ name: "restart_test" }]); + }); beforeEach(function() { // Would not be needed if controller setup was beforeEach - controller.config.restartRequired = false; controller.config.set("controller.version", controllerVersion); - controller.config.set("global_chat.load_plugin", true); + controller.config.set("restart_test.load_plugin", true); + controller.config.restartRequired = false; controller.pluginInfos[0] = { - name: "global_chat", + name: "restart_test", packagePath: require.resolve("@clusterio/controller/package.json"), version: controllerVersion, }; @@ -256,7 +260,7 @@ describe("controller/src/Controller", function() { controller.pluginInfos[0].version = "0.0.0"; controller.pluginInfos[0].webEntrypoint = true; controller.pluginInfos[0].controllerEntrypoint = true; - controller.config.set("global_chat.load_plugin", false); + controller.config.set("restart_test.load_plugin", false); controller.config.restartRequired = false; // Setting load_plugin requires a restart const result = await controller.checkRestartRequired(); assert.equal(result, false); diff --git a/test/host.js b/test/host.js index ad97bd83..5fb6ab5f 100644 --- a/test/host.js +++ b/test/host.js @@ -119,10 +119,13 @@ describe("Host testing", function() { describe(".checkRestartRequired()", function() { let host; const hostVersion = require("@clusterio/host/package.json").version; + before(function() { + lib.addPluginConfigFields([{ name: "restart_test", hostEntrypoint: "host.js" }]); + }); beforeEach(function() { // This can be used more generally, but i did not want to interfere with handleSyncUserListsEvent const pluginInfos = [{ - name: "global_chat", + name: "restart_test", packagePath: require.resolve("@clusterio/host/package.json"), version: hostVersion, }]; @@ -141,7 +144,7 @@ describe("Host testing", function() { host.pluginInfos[0].version = "0.0.0"; host.pluginInfos[0].hostEntrypoint = true; host.pluginInfos[0].instanceEntrypoint = true; - host.config.set("global_chat.load_plugin", false); + host.config.set("restart_test.load_plugin", false); host.config.restartRequired = false; // Setting load_plugin requires a restart const result = await host.checkRestartRequired(); assert.equal(result, false); From 9d061a0f538787cf03421420b21a5cdd1e0e820c Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Mon, 31 Aug 2026 18:49:45 +0200 Subject: [PATCH 08/22] Bump webpack version Use the latest version of webpack and webpack-cli. It's possible that using ESM imports + having an outdated webpack-cli caused issues. --- packages/web_ui/package.json | 4 ++-- pnpm-workspace.yaml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/web_ui/package.json b/packages/web_ui/package.json index 76b2d028..9371bdc6 100644 --- a/packages/web_ui/package.json +++ b/packages/web_ui/package.json @@ -60,8 +60,8 @@ "terser-webpack-plugin": "^5.3.11", "typescript": "^5.7.3", "util": "^0.12.5", - "webpack": "^5.98.0", - "webpack-cli": "^5.1.4", + "webpack": "catalog:", + "webpack-cli": "catalog:", "webpack-manifest-plugin": "^5.0.0" }, "devDependencies": { diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index fefcb0b9..f22b099c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -26,9 +26,9 @@ catalog: react-router-dom: ^6.29.0 set-blocking: ^2.0.0 typescript: ^5.7.3 - webpack: ^5.98.0 - webpack-cli: ^5.1.4 - webpack-merge: ^5.9.0 + webpack: ^5.110.3 + webpack-cli: ^7.2.3 + webpack-merge: 5.9.0 winston: ^3.17.0 winston-daily-rotate-file: ^4.7.1 winston-transport: ^4.9.0 From a025d01294a4403c52b662bb83b2d39d1cb3d3ce Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Fri, 11 Sep 2026 19:44:33 +0200 Subject: [PATCH 09/22] Fix mainBundle being passed as array in dev mode Values in the assetsByChunkName object are arrays not strings. This caused the dev mode to fail after 2f4e0c441852 added an additional step filtering out the static/ prefix from what it expected to be a string. --- packages/controller/src/Controller.ts | 2 +- packages/controller/src/routes.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/controller/src/Controller.ts b/packages/controller/src/Controller.ts index f24469df..d9362ff7 100644 --- a/packages/controller/src/Controller.ts +++ b/packages/controller/src/Controller.ts @@ -1237,7 +1237,7 @@ export default class Controller { mainBundle = res.app.locals.mainBundle; } else { let stats = res.locals.webpack.devMiddleware.stats.stats[0]; - mainBundle = stats.toJson().assetsByChunkName["main"]; + mainBundle = stats.toJson().assetsByChunkName["main"][0]; } mainBundle = routes.stripStaticPrefix(mainBundle); diff --git a/packages/controller/src/routes.ts b/packages/controller/src/routes.ts index a4e1da70..f99d729f 100644 --- a/packages/controller/src/routes.ts +++ b/packages/controller/src/routes.ts @@ -126,7 +126,7 @@ function getPlugins(req: Request, res: Response) { let devPlugins = req.app.locals.devPlugins; if (devPlugins && devPlugins.has(name)) { let stats = res.locals.webpack.devMiddleware.stats.stats[devPlugins.get(name)!]; - web.main = stats.toJson().assetsByChunkName[name]; + web.main = stats.toJson().assetsByChunkName[name][0]; } else if (pluginInfo.manifest) { web.main = pluginInfo.manifest[`${pluginInfo.name}.js`]; if (!web.main) { From b69111dda380483137e8dbdc649b5766d9041009 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Fri, 11 Sep 2026 20:15:57 +0200 Subject: [PATCH 10/22] Fix obsolete usage of EventSubscriber in template This functionality was moved to the MapSubscriber class in e49b82842d88. --- packages/create/templates/plugin-js/web/plugin.jsx | 2 +- packages/create/templates/plugin-ts/web/plugin.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/create/templates/plugin-js/web/plugin.jsx b/packages/create/templates/plugin-js/web/plugin.jsx index 7a67c8f6..4aa14c85 100644 --- a/packages/create/templates/plugin-js/web/plugin.jsx +++ b/packages/create/templates/plugin-js/web/plugin.jsx @@ -43,7 +43,7 @@ function MyTemplatePage() { export class WebPlugin extends BaseWebPlugin { //%if controller // Subscribing requires web content and the controller - subscribableData = new lib.EventSubscriber(ExampleSubscribableUpdate, this.control); + subscribableData = new lib.MapSubscriber(ExampleSubscribableUpdate, this.control); //%endif async init() { diff --git a/packages/create/templates/plugin-ts/web/plugin.tsx b/packages/create/templates/plugin-ts/web/plugin.tsx index d4d9d150..40d1ca9b 100644 --- a/packages/create/templates/plugin-ts/web/plugin.tsx +++ b/packages/create/templates/plugin-ts/web/plugin.tsx @@ -43,7 +43,7 @@ function MyTemplatePage() { export class WebPlugin extends BaseWebPlugin { //%if controller // Subscribing requires web content and the controller - subscribableData = new lib.EventSubscriber(ExampleSubscribableUpdate, this.control); + subscribableData = new lib.MapSubscriber(ExampleSubscribableUpdate, this.control); //%endif async init() { From 897c18bd08287e7d0ec1ac5cbd093275d231764f Mon Sep 17 00:00:00 2001 From: Bastiaan <17990055+bbassie@users.noreply.github.com> Date: Fri, 11 Sep 2026 21:25:45 +0200 Subject: [PATCH 11/22] Resolve numeric names in ctl id lookups (#989) resolveHost, resolveInstance, resolveModPack and retrieveRole treated any integer like argument as an id and never looked it up as a name. An instance created as "10" could not be referenced by name afterwards. The lookup now treats the argument as an id only if an entry with that id exists, and otherwise falls back to a name match. Closes #886 Co-authored-by: Claude Fable 5.1 --- packages/lib/src/command.ts | 100 +++++++++++------------------------- test/lib/command.js | 47 ++++++++++++++--- 2 files changed, 71 insertions(+), 76 deletions(-) diff --git a/packages/lib/src/command.ts b/packages/lib/src/command.ts index 2a55c22e..9b375175 100644 --- a/packages/lib/src/command.ts +++ b/packages/lib/src/command.ts @@ -146,6 +146,22 @@ export class CommandTree { } +/** + * Resolve a string with either an id or a name to the id of an entry + * + * An integer like string is treated as an id if an entry with that id + * exists, otherwise it is looked up as a name. + */ +function resolveId(entries: { id: number, name: string }[], idOrName: string) { + if (/^-?\d+$/.test(idOrName)) { + const id = parseInt(idOrName, 10); + if (entries.some(entry => entry.id === id)) { + return id; + } + } + return entries.find(entry => entry.name === idOrName)?.id; +} + /** * Resolve a string into a host ID * @@ -157,23 +173,11 @@ export class CommandTree { * @returns host ID. */ export async function resolveHost(client: Link, hostName: string) { - let hostId: number | undefined; - if (/^-?\d+$/.test(hostName)) { - hostId = parseInt(hostName, 10); - } else { - let hosts = await client.sendTo("controller", new libData.HostListRequest()); - for (let host of hosts) { - if (host.name === hostName) { - hostId = host.id; - break; - } - } - - if (hostId === undefined) { - throw new libErrors.CommandError(`No host named ${hostName}`); - } + const hosts = await client.sendTo("controller", new libData.HostListRequest()); + const hostId = resolveId(hosts, hostName); + if (hostId === undefined) { + throw new libErrors.CommandError(`No host named ${hostName}`); } - return hostId; } @@ -188,23 +192,11 @@ export async function resolveHost(client: Link, hostName: string) { * @returns instance ID. */ export async function resolveInstance(client: Link, instanceName: string) { - let instanceId: number | undefined; - if (/^-?\d+$/.test(instanceName)) { - instanceId = parseInt(instanceName, 10); - } else { - let instances = await client.sendTo("controller", new libData.InstanceDetailsListRequest()); - for (let instance of instances) { - if (instance.name === instanceName) { - instanceId = instance.id; - break; - } - } - - if (instanceId === undefined) { - throw new libErrors.CommandError(`No instance named ${instanceName}`); - } + const instances = await client.sendTo("controller", new libData.InstanceDetailsListRequest()); + const instanceId = resolveId(instances, instanceName); + if (instanceId === undefined) { + throw new libErrors.CommandError(`No instance named ${instanceName}`); } - return instanceId; } @@ -220,23 +212,11 @@ export async function resolveInstance(client: Link, instanceName: string) { * @returns mod pack ID. */ export async function resolveModPack(client: Link, modPackName: string) { - let modPackId: number | undefined; - if (/^-?\d+/.test(modPackName)) { - modPackId = parseInt(modPackName, 10); - } else { - let modPacks = await client.sendTo("controller", new libData.ModPackListRequest()); - for (let modPack of modPacks) { - if (modPack.name === modPackName) { - modPackId = modPack.id; - break; - } - } - - if (modPackId === undefined) { - throw new libErrors.CommandError(`No mod pack named ${modPackName}`); - } + const modPacks = await client.sendTo("controller", new libData.ModPackListRequest()); + const modPackId = resolveId(modPacks, modPackName); + if (modPackId === undefined) { + throw new libErrors.CommandError(`No mod pack named ${modPackName}`); } - return modPackId; } @@ -251,27 +231,9 @@ export async function resolveModPack(client: Link, modPackName: string) { * @returns Role info. */ export async function retrieveRole(client: Link, roleName: string) { - let roles = await client.sendTo("controller", new libData.RoleListRequest()); - - let resolvedRole: libData.Role | undefined; - if (/^-?\d+$/.test(roleName)) { - let roleId = parseInt(roleName, 10); - for (let role of roles) { - if (role.id === roleId) { - resolvedRole = role; - break; - } - } - - } else { - for (let role of roles) { - if (role.name === roleName) { - resolvedRole = role; - break; - } - } - } - + const roles = await client.sendTo("controller", new libData.RoleListRequest()); + const roleId = resolveId(roles, roleName); + const resolvedRole = roles.find(role => role.id === roleId); if (!resolvedRole) { throw new libErrors.CommandError(`No role named ${roleName}`); } diff --git a/test/lib/command.js b/test/lib/command.js index 86e299c6..40c658e0 100644 --- a/test/lib/command.js +++ b/test/lib/command.js @@ -16,39 +16,68 @@ describe("lib/command", function() { let testControl = new mock.MockControl(controlConnector); let testController = new lib.Link(controllerConnector); testController.handle( - lib.HostListRequest, () => [new lib.HostDetails("0.1", "Test Host", 11, false)] + lib.HostListRequest, () => [ + new lib.HostDetails("0.1", "Test Host", 11, false), + new lib.HostDetails("0.1", "22", 12, false), + new lib.HostDetails("0.1", "12", 13, false), + ] ); testController.handle( - lib.InstanceDetailsListRequest, () => [new lib.InstanceDetails("Test Instance", 57, 4, undefined, "stopped")] + lib.InstanceDetailsListRequest, () => [ + new lib.InstanceDetails("Test Instance", 57, 4, undefined, "stopped"), + new lib.InstanceDetails("10", 58, 4, undefined, "stopped"), + new lib.InstanceDetails("58", 59, 4, undefined, "stopped"), + ] ); - testController.handle(lib.RoleListRequest, () => [testRole.toJSON()]); + let numericRole = lib.Role.fromJSON({ id: 29, name: "77", description: "Test", permissions: [] }); + testController.handle(lib.RoleListRequest, () => [testRole.toJSON(), numericRole.toJSON()]); describe("resolveHost", function() { - it("should pass an integer like string back", async function() { - assert.equal(await lib.resolveHost(null, "123"), 123); + it("should resolve an integer like string as an id", async function() { + assert.equal(await lib.resolveHost(testControl, "11"), 11); }); it("should resolve a host name with the controller", async function() { assert.equal(await lib.resolveHost(testControl, "Test Host"), 11); }); + it("should resolve an integer like name if no host has that id", async function() { + assert.equal(await lib.resolveHost(testControl, "22"), 12); + }); + it("should prefer the id over an integer like name", async function() { + assert.equal(await lib.resolveHost(testControl, "12"), 12); + }); it("should throw if host is not found", async function() { await assert.rejects( lib.resolveHost(testControl, "invalid"), new lib.CommandError("No host named invalid") ); + await assert.rejects( + lib.resolveHost(testControl, "123"), + new lib.CommandError("No host named 123") + ); }); }); describe("resolveInstance", function() { - it("should pass an integer like string back", async function() { - assert.equal(await lib.resolveInstance(null, "123"), 123); + it("should resolve an integer like string as an id", async function() { + assert.equal(await lib.resolveInstance(testControl, "57"), 57); }); it("should resolve an instance name with the controller", async function() { assert.equal(await lib.resolveInstance(testControl, "Test Instance"), 57); }); + it("should resolve an integer like name if no instance has that id", async function() { + assert.equal(await lib.resolveInstance(testControl, "10"), 58); + }); + it("should prefer the id over an integer like name", async function() { + assert.equal(await lib.resolveInstance(testControl, "58"), 58); + }); it("should throw if instance is not found", async function() { await assert.rejects( lib.resolveInstance(testControl, "invalid"), new lib.CommandError("No instance named invalid") ); + await assert.rejects( + lib.resolveInstance(testControl, "123"), + new lib.CommandError("No instance named 123") + ); }); }); describe("retrieveRole", function() { @@ -58,6 +87,10 @@ describe("lib/command", function() { it("should retrieve a role by name from a string", async function() { assert.deepEqual(await lib.retrieveRole(testControl, "Test Role"), testRole); }); + it("should retrieve a role by an integer like name if no role has that id", async function() { + assert.deepEqual(await lib.retrieveRole(testControl, "28"), testRole); + assert.deepEqual(await lib.retrieveRole(testControl, "77"), numericRole); + }); it("should throw if role is not found", async function() { await assert.rejects( lib.retrieveRole(testControl, "invalid"), From bb36af08fe90ef4aca485745b974f00209e17957 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Fri, 11 Sep 2026 19:26:38 +0000 Subject: [PATCH 12/22] Apply recipe notification deltas directly in Lua Review feedback: the download field now always carries the delta and the deserializer applies it against the current state instead of rebuilding the full list first. The previous failed recipe names are passed in so they survive the round trip. The version check moved to the call site. Co-Authored-By: Claude Fable 5.1 --- plugins/inventory_sync/instance.ts | 14 +-- plugins/inventory_sync/messages.ts | 1 - .../inventory_sync/module/inventory_sync.lua | 24 ++-- plugins/inventory_sync/module/serialize.lua | 109 ++++++------------ plugins/inventory_sync/test/instance.js | 30 ++--- 5 files changed, 69 insertions(+), 109 deletions(-) diff --git a/plugins/inventory_sync/instance.ts b/plugins/inventory_sync/instance.ts index 39ddea17..32324517 100644 --- a/plugins/inventory_sync/instance.ts +++ b/plugins/inventory_sync/instance.ts @@ -217,19 +217,19 @@ export class InstancePlugin extends BaseInstancePlugin { * Replace the stored recipe notifications with the difference from the * current state on the instance to reduce the amount of data sent to Lua. */ - async applyRecipeNotificationDelta(playerData: IpcPlayerData, current: string) { + async applyRecipeNotificationDelta(playerData: IpcPlayerData, current?: string) { if (!playerData.recipe_notifications) { return; } try { const delta = recipeNotificationDelta( JSON.parse(await decodeLuaString(playerData.recipe_notifications)), - JSON.parse(await decodeLuaString(current)), + current ? JSON.parse(await decodeLuaString(current)) : [], ); - playerData.recipe_notifications_delta = await encodeLuaString(JSON.stringify(delta)); - delete playerData.recipe_notifications; + playerData.recipe_notifications = await encodeLuaString(JSON.stringify(delta)); } catch (err: any) { - this.logger.warn(`Sending full recipe notifications for ${playerData.name}:\n${err.stack}`); + this.logger.warn(`Dropping invalid recipe notifications for ${playerData.name}:\n${err.stack}`); + delete playerData.recipe_notifications; } } @@ -247,9 +247,7 @@ export class InstancePlugin extends BaseInstancePlugin { return; } - if (request.recipe_notifications) { - await this.applyRecipeNotificationDelta(response.playerData, request.recipe_notifications); - } + await this.applyRecipeNotificationDelta(response.playerData, request.recipe_notifications); const chunkSize = this.instance.config.get("inventory_sync.rcon_chunk_size"); const chunks = chunkify(chunkSize, JSON.stringify(response.playerData)); diff --git a/plugins/inventory_sync/messages.ts b/plugins/inventory_sync/messages.ts index 5585fe55..028c39e0 100644 --- a/plugins/inventory_sync/messages.ts +++ b/plugins/inventory_sync/messages.ts @@ -21,7 +21,6 @@ export type IpcPlayerData = { personal_logistic_slots?: {name:string, min:number, max:number}[], crafting_queue?: any, recipe_notifications?: string, - recipe_notifications_delta?: string, } const jsonPlayerData = Type.Object({ diff --git a/plugins/inventory_sync/module/inventory_sync.lua b/plugins/inventory_sync/module/inventory_sync.lua index 6df45f77..89250148 100644 --- a/plugins/inventory_sync/module/inventory_sync.lua +++ b/plugins/inventory_sync/module/inventory_sync.lua @@ -17,6 +17,7 @@ local progress_dialog = require("modules/inventory_sync/gui/progress_dialog") local dialog_failed_download = require("modules/inventory_sync/gui/dialog_failed_download") local v2_remote_controller = compat.version_ge("2.0.0") +local recipe_notifications_api = compat.version_ge("2.0.67") -- Returns true if the player is currently in a cutscene local function is_in_cutscene(player) @@ -111,14 +112,9 @@ function inventory_sync.deserialize_player(player, finished_record) -- Deserialize downloaded player data local serialized_player = compat.json_to_table(finished_record.data) assert(type(serialized_player) == "table", "wrong type for serialized_player") - if serialized_player.recipe_notifications_delta then - serialized_player.recipe_notifications = serialize.apply_crafting_notification_delta( - assert(finished_record.recipe_notifications, "recipe notification delta without snapshot"), - serialized_player.recipe_notifications_delta - ) - serialized_player.recipe_notifications_delta = nil - end - script_data.failed_deserialization[player.name] = serialize.deserialize_player(player, serialized_player) + script_data.failed_deserialization[player.name] = serialize.deserialize_player( + player, serialized_player, script_data.failed_deserialization[player.name] + ) -- Restore player position and driving state restore_position(player, finished_record) @@ -449,13 +445,17 @@ function inventory_sync.initiate_inventory_download(player, player_record, gener script_data.active_downloads[player.name] = record -- The plugin only sends back what differs from the current notification state - record.recipe_notifications = serialize.crafting_notification_snapshot( - player, script_data.failed_deserialization[player.name] - ) + local recipe_notifications + if recipe_notifications_api then + local failed = script_data.failed_deserialization[player.name] + recipe_notifications = serialize.serialize_crafting_notifications( + player, failed and failed.recipe_notifications + ) + end clusterio_api.send_json("inventory_sync_download", { player_name = player.name, - recipe_notifications = record.recipe_notifications, + recipe_notifications = recipe_notifications, }) -- If this is a synced player turn them into a spectator while the diff --git a/plugins/inventory_sync/module/serialize.lua b/plugins/inventory_sync/module/serialize.lua index 78430de3..7fabb07c 100644 --- a/plugins/inventory_sync/module/serialize.lua +++ b/plugins/inventory_sync/module/serialize.lua @@ -545,95 +545,54 @@ function serialize.serialize_crafting_notifications(player, failed) return helpers.encode_string(helpers.table_to_json(recipe_names)) end ---- Current notification state, sent with download requests so the instance plugin only returns the difference ---- @param player LuaPlayer ---- @param failed FailedDeserializationPlayerData? ---- @return string? -function serialize.crafting_notification_snapshot(player, failed) - if not recipe_notifications_api then - return nil - end - return serialize.serialize_crafting_notifications(player, failed and failed.recipe_notifications) -end - --- @class CraftingNotificationDelta ---- @field add string[]? Recipe names to add to the cleared list ---- @field remove string[]? Recipe names to remove from the cleared list - ---- Rebuild the full serialized form from the snapshot the delta was computed against ---- @param snapshot string ---- @param serialized_delta string ---- @return string? -function serialize.apply_crafting_notification_delta(snapshot, serialized_delta) - local recipe_names = helpers.json_to_table(assert(helpers.decode_string(snapshot))) - assert(type(recipe_names) == "table", "wrong type decoded from json_to_table") - --- @type CraftingNotificationDelta - local delta = helpers.json_to_table(assert(helpers.decode_string(serialized_delta))) - assert(type(delta) == "table", "wrong type decoded from json_to_table") - local cleared = {} - for _, name in pairs(recipe_names) do - cleared[name] = true - end - for _, name in pairs(delta.remove or {}) do - cleared[name] = nil - end - for _, name in pairs(delta.add or {}) do - cleared[name] = true - end - - local merged = {} - for name in pairs(cleared) do - merged[#merged + 1] = name - end - return helpers.encode_string(helpers.table_to_json(merged)) -end +--- @field add string[]? Recipes cleared elsewhere that still have a notification here +--- @field remove string[]? Recipes with a notification elsewhere that are cleared here --- @param player LuaPlayer ---- @param serialized string +--- @param serialized string Encoded delta against the state sent with the download request +--- @param previous_failed string[]? Recipe names that did not exist at the last deserialization --- @return string[]? failed Recipe names that do not exist -function serialize.deserialize_crafting_notifications(player, serialized) - -- Decode the recipe name strings - local recipe_names = helpers.json_to_table(assert(helpers.decode_string(serialized))) - assert(type(recipe_names) == "table", "wrong type decoded from json_to_table") - local clear_notification = player.clear_recipe_notification - local add_notification = player.add_recipe_notification - local notifications = {} +function serialize.deserialize_crafting_notifications(player, serialized, previous_failed) + --- @type CraftingNotificationDelta + local delta = helpers.json_to_table(assert(helpers.decode_string(serialized))) + assert(type(delta) == "table", "wrong type decoded from json_to_table") local failed = {} + for _, recipe_name in pairs(previous_failed or {}) do + failed[recipe_name] = true + end - -- Assume all recipes need a notification + -- Recipes which can have a notification on this server + local enabled = {} for _, recipe in pairs(player.force.recipes) do if recipe.enabled and not recipe.hidden then - notifications[recipe.name] = true + enabled[recipe.name] = true end end - -- Remove recipes the player has cleared - for _, recipe_name in pairs(recipe_names) do - if notifications[recipe_name] then - notifications[recipe_name] = false + -- Clear notifications the player has cleared elsewhere + for _, recipe_name in pairs(delta.add or {}) do + if enabled[recipe_name] then + player.clear_recipe_notification(recipe_name) else - -- Recipe does not exist on this server - failed[#failed+1] = recipe_name + failed[recipe_name] = true -- Recipe does not exist on this server end end - -- Clear ones which are ones which are not required - for _, recipe in pairs(player.get_recipe_notifications()) do - if notifications[recipe.name] == false then - clear_notification(recipe.name) - end - notifications[recipe.name] = nil -- Prevents double add below - end - - -- Add any which the player did not have - for recipe_name, should_add in pairs(notifications) do - if should_add then - add_notification(recipe_name) + -- Add notifications the player has elsewhere + for _, recipe_name in pairs(delta.remove or {}) do + if enabled[recipe_name] then + player.add_recipe_notification(recipe_name) + else + failed[recipe_name] = nil end end - -- Return failed recipes if any - return next(failed) and failed or nil + local failed_names = {} + for recipe_name in pairs(failed) do + failed_names[#failed_names + 1] = recipe_name + end + return next(failed_names) and failed_names or nil end local controller_to_name = {} @@ -839,8 +798,9 @@ end --- @param player LuaPlayer --- @param serialized SerializedPlayerData +--- @param previous_failed FailedDeserializationPlayerData? --- @return FailedDeserializationPlayerData? -function serialize.deserialize_player(player, serialized) +function serialize.deserialize_player(player, serialized, previous_failed) local failed_deserialization = {} local target_controller = defines.controllers[serialized.controller] @@ -927,8 +887,9 @@ function serialize.deserialize_player(player, serialized) -- Deserialize recipe notifications if recipe_notifications_api and serialized.recipe_notifications then - failed_deserialization.recipe_notifications = - serialize.deserialize_crafting_notifications(player, serialized.recipe_notifications) + failed_deserialization.recipe_notifications = serialize.deserialize_crafting_notifications( + player, serialized.recipe_notifications, previous_failed and previous_failed.recipe_notifications + ) end return next(failed_deserialization) and failed_deserialization or nil diff --git a/plugins/inventory_sync/test/instance.js b/plugins/inventory_sync/test/instance.js index e292c19f..298a89ad 100644 --- a/plugins/inventory_sync/test/instance.js +++ b/plugins/inventory_sync/test/instance.js @@ -48,25 +48,27 @@ describe("inventory_sync", function() { it("should replace the stored list with an encoded delta", async function() { const playerData = { name: "test", recipe_notifications: await encode(["a", "b"]) }; await apply(playerData, await encode(["b", "c"])); - assert.equal(playerData.recipe_notifications, undefined); - assert.deepEqual(await decode(playerData.recipe_notifications_delta), { add: ["a"], remove: ["c"] }); + assert.deepEqual(await decode(playerData.recipe_notifications), { add: ["a"], remove: ["c"] }); }); it("should send an empty delta when nothing changed", async function() { const playerData = { name: "test", recipe_notifications: await encode(["a"]) }; await apply(playerData, await encode(["a"])); - assert.deepEqual(await decode(playerData.recipe_notifications_delta), {}); + assert.deepEqual(await decode(playerData.recipe_notifications), {}); + }); + it("should add everything without a current list", async function() { + const playerData = { name: "test", recipe_notifications: await encode(["a"]) }; + await apply(playerData, undefined); + assert.deepEqual(await decode(playerData.recipe_notifications), { add: ["a"] }); }); it("should do nothing without stored notifications", async function() { const playerData = { name: "test" }; await apply(playerData, await encode(["a"])); assert.deepEqual(playerData, { name: "test" }); }); - it("should fall back to the full list on invalid input", async function() { - const stored = await encode(["a"]); - const playerData = { name: "test", recipe_notifications: stored }; - await apply(playerData, "not base64 zlib"); - assert.equal(playerData.recipe_notifications, stored); - assert.equal(playerData.recipe_notifications_delta, undefined); + it("should drop the field on invalid input", async function() { + const playerData = { name: "test", recipe_notifications: "not base64 zlib" }; + await apply(playerData, await encode(["a"])); + assert.equal(playerData.recipe_notifications, undefined); assert.equal(warnings.length, 1); }); }); @@ -106,16 +108,16 @@ describe("inventory_sync", function() { ["/sc inventory_sync.download_inventory('test',nil,0,0)"] ); }); - it("should send the full list when no snapshot was given", async function() { - const playerData = { generation: 1, name: "test", recipe_notifications: await encode(["a"]) }; - respondWith({ ...playerData }); + it("should send everything as a delta when no snapshot was given", async function() { + respondWith({ generation: 1, name: "test", recipe_notifications: await encode(["a"]) }); await instancePlugin.handleDownload({ player_name: "test" }); - assert.deepEqual(instancePlugin.instance.server.rconCommands, [downloadCommand(playerData)]); + const expected = { generation: 1, name: "test", recipe_notifications: await encode({ add: ["a"] }) }; + assert.deepEqual(instancePlugin.instance.server.rconCommands, [downloadCommand(expected)]); }); it("should send a delta when a snapshot was given", async function() { respondWith({ generation: 1, name: "test", recipe_notifications: await encode(["a", "b"]) }); await instancePlugin.handleDownload({ player_name: "test", recipe_notifications: await encode(["b"]) }); - const expected = { generation: 1, name: "test", recipe_notifications_delta: await encode({ add: ["a"] }) }; + const expected = { generation: 1, name: "test", recipe_notifications: await encode({ add: ["a"] }) }; assert.deepEqual(instancePlugin.instance.server.rconCommands, [downloadCommand(expected)]); }); it("should be invoked by the download ipc", async function() { From 5dcaca4d0b3642bc2cbb4313426cafa3a5430fe5 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Thu, 10 Sep 2026 20:10:44 +0200 Subject: [PATCH 13/22] 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. --- clean.js | 11 +- docs/contributing.md | 4 +- docs/devs/json-serialisable-classes.md | 11 +- docs/writing-plugins.md | 30 ++--- eslint.config.mjs | 4 +- package.json | 1 + packages/controller/controller.ts | 18 +-- packages/controller/index.ts | 28 ++--- packages/controller/package.json | 1 + packages/controller/src/BaseConnection.ts | 8 +- .../controller/src/BaseControllerPlugin.ts | 8 +- packages/controller/src/ControlConnection.ts | 16 ++- packages/controller/src/Controller.ts | 62 ++++++---- packages/controller/src/ControllerRouter.ts | 10 +- packages/controller/src/HostConnection.ts | 12 +- packages/controller/src/InstanceManager.ts | 6 +- packages/controller/src/User.ts | 2 +- packages/controller/src/UserManager.ts | 4 +- packages/controller/src/WsServer.ts | 23 ++-- packages/controller/src/WsServerConnector.ts | 8 +- packages/controller/src/routes.ts | 4 +- .../{webpack.config.js => webpack.config.cjs} | 0 packages/create/create.js | 33 +++-- packages/create/escape_arg.js | 8 +- packages/create/logging.js | 19 ++- packages/create/package.json | 1 + packages/create/template.js | 24 ++-- packages/create/templates/common/package.json | 1 + .../{webpack.config.js => webpack.config.cjs} | 0 .../create/templates/plugin-js/controller.js | 15 +-- packages/create/templates/plugin-js/ctl.js | 13 +- packages/create/templates/plugin-js/host.js | 13 +- packages/create/templates/plugin-js/index.js | 19 ++- .../create/templates/plugin-js/instance.js | 13 +- .../templates/plugin-js/instance_empty.js | 9 +- .../create/templates/plugin-js/messages.js | 22 +--- .../create/templates/plugin-js/web/plugin.jsx | 2 +- .../create/templates/plugin-ts/controller.ts | 2 +- packages/create/templates/plugin-ts/ctl.ts | 2 +- packages/create/templates/plugin-ts/host.ts | 2 +- packages/create/templates/plugin-ts/index.ts | 10 +- .../create/templates/plugin-ts/instance.ts | 2 +- .../create/templates/plugin-ts/web/plugin.tsx | 2 +- packages/ctl/ctl.ts | 12 +- packages/ctl/index.ts | 8 +- packages/ctl/package.json | 1 + packages/ctl/src/commands.ts | 22 ++-- packages/ctl/src/commands_controller.ts | 6 +- packages/ctl/src/commands_debug.ts | 2 +- packages/ctl/src/commands_host.ts | 6 +- packages/ctl/src/commands_instance.ts | 6 +- packages/ctl/src/commands_log.ts | 2 +- packages/ctl/src/commands_mod.ts | 4 +- packages/ctl/src/commands_mod_pack.ts | 4 +- packages/ctl/src/commands_permission.ts | 4 +- packages/ctl/src/commands_role.ts | 4 +- packages/ctl/src/commands_user.ts | 4 +- .../ctl/src/set_lazy_schema_compliation.ts | 3 + packages/host/host.ts | 12 +- packages/host/index.ts | 14 +-- packages/host/package.json | 1 + packages/host/src/BaseHostPlugin.ts | 2 +- packages/host/src/BaseInstancePlugin.ts | 4 +- packages/host/src/Host.ts | 15 +-- packages/host/src/Instance.ts | 12 +- packages/host/src/InstanceConnection.ts | 4 +- packages/host/src/export.ts | 8 +- packages/host/src/patch.ts | 2 +- packages/lib/browser.ts | 2 +- packages/lib/build_mod.js | 21 ++-- packages/lib/index.ts | 78 ++++++------ packages/lib/package.json | 11 ++ packages/lib/scripts/compile_validator.js | 16 +-- packages/lib/src/ModStore.ts | 8 +- packages/lib/src/check_singleton_import.ts | 2 +- packages/lib/src/command.ts | 6 +- packages/lib/src/config/classes.ts | 14 +-- packages/lib/src/config/definitions.ts | 8 +- packages/lib/src/config/index.ts | 4 +- packages/lib/src/data/InstanceDetails.ts | 4 +- packages/lib/src/data/ModInfo.ts | 10 +- packages/lib/src/data/ModPack.ts | 14 +-- packages/lib/src/data/ModuleInfo.ts | 2 +- packages/lib/src/data/Role.ts | 2 +- packages/lib/src/data/UserDetails.ts | 2 +- packages/lib/src/data/index.ts | 46 +++---- packages/lib/src/data/message_validate.d.ts | 2 +- packages/lib/src/data/messages_controller.ts | 8 +- packages/lib/src/data/messages_core.ts | 4 +- packages/lib/src/data/messages_host.ts | 8 +- packages/lib/src/data/messages_instance.ts | 16 +-- packages/lib/src/data/messages_mod.ts | 8 +- packages/lib/src/data/messages_plugin.ts | 4 +- packages/lib/src/data/messages_role.ts | 6 +- packages/lib/src/data/messages_user.ts | 6 +- packages/lib/src/database.ts | 4 +- packages/lib/src/datastore.ts | 8 +- packages/lib/src/export.ts | 2 +- packages/lib/src/external/FactorioVersions.ts | 2 +- packages/lib/src/external/LatestReleases.ts | 2 +- packages/lib/src/external/index.ts | 4 +- packages/lib/src/factorio/index.ts | 2 +- packages/lib/src/file_ops.ts | 2 +- packages/lib/src/helpers.ts | 2 +- .../lib/src/is_deep_strict_equal.browser.ts | 1 + packages/lib/src/is_deep_strict_equal.d.ts | 1 - packages/lib/src/is_deep_strict_equal.js | 9 -- packages/lib/src/is_deep_strict_equal.node.ts | 1 + packages/lib/src/link/WebSocket.browser.ts | 2 + packages/lib/src/link/WebSocket.d.ts | 2 - packages/lib/src/link/WebSocket.js | 3 - packages/lib/src/link/WebSocket.node.ts | 1 + packages/lib/src/link/connectors.ts | 23 ++-- packages/lib/src/link/index.ts | 4 +- packages/lib/src/link/link.ts | 16 +-- packages/lib/src/link/messages.ts | 20 +-- packages/lib/src/load_plugin_list.ts | 5 +- packages/lib/src/logging_utils.ts | 12 +- packages/lib/src/permissions.ts | 2 +- packages/lib/src/plugin.ts | 8 +- packages/lib/src/plugin_loader.ts | 19 +-- packages/lib/src/prometheus.ts | 10 +- packages/lib/src/rce_ops.ts | 8 +- packages/lib/src/schema.ts | 2 +- packages/lib/src/shared_commands.ts | 20 +-- packages/lib/src/subscriptions.ts | 10 +- packages/lib/src/system_collectors.ts | 4 +- packages/lib/tsconfig.browser.json | 1 + packages/lib/tsconfig.node.json | 1 + packages/web_ui/webpack.common.js | 3 + plugins/global_chat/control.ts | 2 +- plugins/global_chat/index.ts | 6 +- plugins/global_chat/instance.ts | 2 +- plugins/global_chat/package.json | 1 + plugins/global_chat/test/plugin.js | 15 +-- .../{webpack.config.js => webpack.config.cjs} | 0 plugins/inventory_sync/controller.ts | 4 +- plugins/inventory_sync/index.ts | 6 +- plugins/inventory_sync/instance.ts | 2 +- plugins/inventory_sync/package.json | 1 + plugins/inventory_sync/web/index.tsx | 2 +- .../{webpack.config.js => webpack.config.cjs} | 0 plugins/player_auth/controller.ts | 7 +- plugins/player_auth/index.ts | 6 +- plugins/player_auth/instance.ts | 2 +- plugins/player_auth/package.json | 1 + plugins/player_auth/test/plugin.js | 19 ++- plugins/player_auth/web/index.tsx | 2 +- .../{webpack.config.js => webpack.config.cjs} | 0 plugins/research_sync/controller.ts | 4 +- plugins/research_sync/index.ts | 6 +- plugins/research_sync/instance.ts | 2 +- plugins/research_sync/package.json | 1 + plugins/research_sync/test/missing.js | 5 +- .../{webpack.config.js => webpack.config.cjs} | 0 plugins/statistics_exporter/index.ts | 2 +- plugins/statistics_exporter/instance.ts | 2 +- plugins/statistics_exporter/package.json | 1 + plugins/statistics_exporter/test/plugin.js | 9 +- .../{webpack.config.js => webpack.config.cjs} | 0 plugins/subspace_storage/controller.ts | 8 +- plugins/subspace_storage/dole.ts | 4 +- plugins/subspace_storage/index.ts | 6 +- plugins/subspace_storage/instance.ts | 2 +- plugins/subspace_storage/package.json | 1 + plugins/subspace_storage/test/missing.js | 9 +- plugins/subspace_storage/test/routes.js | 7 +- plugins/subspace_storage/web/index.tsx | 2 +- .../{webpack.config.js => webpack.config.cjs} | 0 pnpm-workspace.yaml | 5 +- publish.js | 9 +- scripts/version-compat-test.js | 14 +-- test/common.js | 14 +-- test/controller/BaseControllerPlugin.js | 5 +- test/controller/ControlConnection.test.js | 5 +- test/controller/Controller.js | 27 ++-- test/controller/ControllerUser.js | 7 +- test/controller/HostRecord.test.js | 9 +- test/controller/HttpCloser.js | 15 ++- test/controller/InstanceManager.test.js | 7 +- test/controller/InstanceRecord.test.js | 9 +- test/controller/User.test.js | 7 +- test/controller/UserManager.test.js | 13 +- test/controller/UserRecord.test.js | 9 +- test/controller/WsServer.js | 7 +- test/controller/routes.js | 15 ++- test/create/echo_args.js | 3 +- test/create/escape_arg.js | 13 +- test/file/test_plugin/control.js | 11 +- test/file/test_plugin/controller.js | 11 +- test/file/test_plugin/host.js | 11 +- test/file/test_plugin/index.js | 31 +++-- test/file/test_plugin/instance.js | 9 +- test/file/test_plugin/messages.js | 15 +-- test/file/test_plugin/package.json | 1 + test/host.js | 29 +++-- test/host/BaseHostPlugin.js | 6 +- test/host/BaseInstancePlugin.js | 7 +- test/host/Instance.js | 13 +- test/host/export.js | 9 +- test/host/patch.js | 13 +- test/host/server.js | 20 ++- test/integration/clusterio.js | 25 ++-- test/integration/create.js | 15 ++- test/integration/export.js | 11 +- test/integration/index.js | 116 +++++++----------- test/integration/instance.js | 15 ++- test/integration/link.js | 12 +- test/integration/patch.js | 17 ++- test/integration/routing.js | 11 +- test/integration/server.js | 19 ++- test/lib/ExponentialBackoff.js | 5 +- test/lib/LockFile.test.js | 9 +- test/lib/ModStore.test.js | 15 ++- test/lib/RateLimiter.js | 5 +- test/lib/ValueCache.test.js | 4 +- test/lib/check_singleton_import.test.js | 8 +- test/lib/command.js | 7 +- test/lib/config/classes.js | 9 +- test/lib/config/migrations.js | 5 +- test/lib/config/validators.test.js | 7 +- test/lib/data/HostDetails.test.js | 7 +- test/lib/data/InstanceDetails.test.js | 7 +- test/lib/data/ModInfo.js | 15 ++- test/lib/data/ModPack.js | 9 +- test/lib/data/Role.test.js | 5 +- test/lib/data/SystemInfo.js | 6 +- test/lib/data/UserDetails.test.js | 7 +- test/lib/data/messages_core.js | 6 +- test/lib/data/version.test.js | 7 +- test/lib/database.js | 9 +- test/lib/datastore.js | 11 +- test/lib/errors.js | 5 +- test/lib/external/FactorioVersions.test.js | 8 +- test/lib/external/LatestReleases.test.js | 6 +- test/lib/factorio/exchange_string.js | 11 +- test/lib/factorio/lines.js | 8 +- test/lib/factorio/test_settings.js | 34 ++--- test/lib/factorio/test_strings.js | 22 +--- test/lib/file_ops.js | 13 +- test/lib/hash.js | 9 +- test/lib/helpers.js | 5 +- test/lib/ini.js | 7 +- test/lib/link/connector.js | 9 +- test/lib/link/link.js | 9 +- test/lib/logging_utils.js | 11 +- test/lib/lua_tools.js | 5 +- test/lib/plugin.js | 7 +- test/lib/plugin_loader.js | 30 ++--- test/lib/prometheus.js | 5 +- test/lib/rce_ops.js | 5 +- test/lib/stream.js | 13 +- test/lib/subscriptions.test.js | 11 +- test/lib/users.js | 5 +- test/lib/zip_ops.js | 7 +- test/manual/test_settings_mod_pack.js | 3 +- test/messages/controller.test.js | 15 ++- test/messages/host.test.js | 9 +- test/messages/mod.test.js | 13 +- test/messages/plugin.test.js | 9 +- test/mock.js | 46 +++---- 261 files changed, 1115 insertions(+), 1330 deletions(-) rename packages/controller/{webpack.config.js => webpack.config.cjs} (100%) rename packages/create/templates/common/{webpack.config.js => webpack.config.cjs} (100%) create mode 100644 packages/ctl/src/set_lazy_schema_compliation.ts create mode 100644 packages/lib/src/is_deep_strict_equal.browser.ts delete mode 100644 packages/lib/src/is_deep_strict_equal.d.ts delete mode 100644 packages/lib/src/is_deep_strict_equal.js create mode 100644 packages/lib/src/is_deep_strict_equal.node.ts create mode 100644 packages/lib/src/link/WebSocket.browser.ts delete mode 100644 packages/lib/src/link/WebSocket.d.ts delete mode 100644 packages/lib/src/link/WebSocket.js create mode 100644 packages/lib/src/link/WebSocket.node.ts rename plugins/global_chat/{webpack.config.js => webpack.config.cjs} (100%) rename plugins/inventory_sync/{webpack.config.js => webpack.config.cjs} (100%) rename plugins/player_auth/{webpack.config.js => webpack.config.cjs} (100%) rename plugins/research_sync/{webpack.config.js => webpack.config.cjs} (100%) rename plugins/statistics_exporter/{webpack.config.js => webpack.config.cjs} (100%) rename plugins/subspace_storage/{webpack.config.js => webpack.config.cjs} (100%) diff --git a/clean.js b/clean.js index f657c54f..51bc9f47 100644 --- a/clean.js +++ b/clean.js @@ -6,12 +6,11 @@ * clean-all: Removes all build, test, and execution artifacts. Including cluster config and logs! */ -"use strict"; -const path = require("path"); -const fs = require("node:fs/promises"); -const yargs = require("yargs"); +import path from "node:path"; +import fs from "node:fs/promises"; +import yargs from "yargs"; -const npmPackage = require("./package.json"); +import npmPackage from "./package.json" with { type: "json" }; const workspaces = npmPackage.workspaces .map(p => p.slice(0, -2)) .filter(p => !p.includes("external_plugins")); @@ -116,6 +115,6 @@ async function main() { } // Run main if started from command line -if (module === require.main) { +if (import.meta.main) { main(); } diff --git a/docs/contributing.md b/docs/contributing.md index 1ae0d1d2..62dc3c71 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -229,8 +229,8 @@ Files containing and exporting a single class should be named the same as the cl Relative imports in lib package are prefixed with lib to make them easier to distinguish, as the names tend to be very generic. E.g.: - const libLink = require("./link"); - const libLuaTools = require("./lua_tools"); + import libLink from "./link"; + import libLuaTools from "./lua_tools"; Note that no sub-modules of the lib package is imported directly from the outside of it with the exception of `build_mod`. diff --git a/docs/devs/json-serialisable-classes.md b/docs/devs/json-serialisable-classes.md index ca152c2a..24715bd4 100644 --- a/docs/devs/json-serialisable-classes.md +++ b/docs/devs/json-serialisable-classes.md @@ -14,7 +14,7 @@ See the following example code for a hypothetical `Data` class with a single pro * A Data class example * @alias module:Data */ -class Data { +export class Data { /** @type {string} */ member = "foo"; @@ -41,8 +41,6 @@ class Data { return json; } } - -module.exports = Data; ``` Serialising an instance of `Data` for JSON is as simple as doing `serialised = JSON.stringify(data)` as the `toJSON` method is automatically called by `JSON.stringify` to create a suitable object. @@ -53,14 +51,13 @@ For example a class holding a data property with an instance of the Data class f ```js // Composed.js -"use strict"; -const Data = require("./Data"); +import Data from "./Data"; /** * Composed data class example * @alias module:Data */ -class Composed { +export class Composed { /** @type {module:Data|undefined} */ data; @@ -83,8 +80,6 @@ class Composed { // toJSON is optional and not needed for this particular class } - -module.exports = Composed; ``` It's easy to create a new instance of Composed by calling `new Composed(new Data("spam"))` and serialising to and from JSON is the same as for Data. diff --git a/docs/writing-plugins.md b/docs/writing-plugins.md index fb80fef8..f86e9582 100644 --- a/docs/writing-plugins.md +++ b/docs/writing-plugins.md @@ -48,12 +48,12 @@ By default the main entrypoint is the `index.js` file, but this may be changed b Here's an example of `index.js`: ```js -module.exports.plugin = { +export const plugin = { name: "foo_frobber", title: "Foo Frobber", description: "Does advanced frobnication", - instanceEntrypoint: "instance", - controllerEntrypoint: "controller", + instanceEntrypoint: "instance.js", + controllerEntrypoint: "controller.js", messages: { /* See below */ }, @@ -117,9 +117,9 @@ The plugin class should derive from its respective base class defined in `lib/pl For example, to define a ControllerPlugin class the following code can be used: ```js -const { BaseControllerPlugin } = require("@clusterio/controller"); +import { BaseControllerPlugin } from "@clusterio/controller"; -class ControllerPlugin extends BaseControllerPlugin { +export class ControllerPlugin extends BaseControllerPlugin { async init() { this.foo = 42; await this.startFrobnication(); @@ -127,10 +127,6 @@ class ControllerPlugin extends BaseControllerPlugin { // ... } - -module.exports = { - ControllerPlugin, -} ``` For the instance plugin it's exactly the same except "Controller" is replaced with "Instance", for host plugins "Host" is used and for the clusterioctl plugin "Ctl" is used. @@ -525,7 +521,7 @@ In its simplest form collecting data from plugins consists of defining the metri For example: ```js -const { Counter } = require("@clusterio/lib"); +import { Counter } from "@clusterio/lib"; const fooMetric = new Counter( "clusterio_foo_frobber_foo_metric", "Measures the level of foo", @@ -541,7 +537,7 @@ It's recommended that plugin metrics follow `clusterio__", "Do frobnications", (yargs) => { @@ -600,15 +596,11 @@ Note that messages sent from clusterioctl needs to have `"control-controller"` a To have the command tree become part of clusterioctl it needs to be added to the rootCommand tree in `addCommands` callback of the Ctl plugin: ```js -const { BaseCtlPlugin } = require("@clusterio/ctl"); +import { BaseCtlPlugin } from "@clusterio/ctl"; -class CtlPlugin extends BaseCtlPlugin { +export class CtlPlugin extends BaseCtlPlugin { async addCommands(rootCommand) { rootCommand.add(fooFrobberCommands); } } - -module.exports = { - CtlPlugin, -} ``` diff --git a/eslint.config.mjs b/eslint.config.mjs index d7b7e8e2..8fc661f1 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -418,7 +418,7 @@ export default defineConfig([{ ...globals.mocha, }, - sourceType: "commonjs", + sourceType: "module", parserOptions: {}, }, @@ -489,7 +489,7 @@ export default defineConfig([{ "node/shebang": "off", }, }, { - files: ["packages/create/templates/**/*.{t,j}s?(x)"], + files: ["packages/create/templates/**/*.?(c){t,j}s?(x)"], "rules": { "node/no-missing-require": "off", diff --git a/package.json b/package.json index dc551a8c..972d08c9 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "private": true, "files": [], "packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316", + "type": "module", "scripts": { "build": "tsc --build", "watch": "tsc --build --watch", diff --git a/packages/controller/controller.ts b/packages/controller/controller.ts index 9787bab6..4c344ba9 100644 --- a/packages/controller/controller.ts +++ b/packages/controller/controller.ts @@ -27,12 +27,12 @@ import jwt from "jsonwebtoken"; // homebrew modules import * as lib from "@clusterio/lib"; -const { ConsoleTransport, levels, logger } = lib; +import { ConsoleTransport, levels, logger } from "@clusterio/lib"; -import Controller from "./src/Controller"; -import UserManager from "./src/UserManager"; -import UserRecord from "./src/UserRecord"; -import { version } from "./package.json"; +import Controller from "./src/Controller.js"; +import UserManager from "./src/UserManager.js"; +import UserRecord from "./src/UserRecord.js"; +import packageConfig from "./package.json" with { type: "json" }; // globals let controller: Controller; @@ -216,7 +216,7 @@ async function handleCopyStaticCommand( pluginInfos: lib.PluginNodeEnvInfo[], ) { await fs.cp( - path.join(__dirname, "..", "web", "static"), + path.join(import.meta.dirname, "..", "web", "static"), args.target, { recursive: true, @@ -377,7 +377,7 @@ async function initialize(): Promise { let command = args._[0]; let shouldRun = false; if (command === "run") { - logger.info(`Starting Clusterio controller ${version}`); + logger.info(`Starting Clusterio controller ${packageConfig.version}`); if (args.recovery) { logger.warn("Controller recovery mode enabled. Some features will be disabled."); } @@ -426,7 +426,7 @@ async function initialize(): Promise { } } - controllerConfig.set("controller.version", version); // Allows tracking last loaded version + controllerConfig.set("controller.version", packageConfig.version); // Allows tracking last loaded version if (!controllerConfig.get("controller.auth_secret")) { logger.info("Generating new controller authentication secret"); @@ -537,6 +537,6 @@ ${err.stack}` }); } -if (module === require.main) { +if (import.meta.main) { bootstrap(); } diff --git a/packages/controller/index.ts b/packages/controller/index.ts index 4e7b2b63..82b2cbad 100644 --- a/packages/controller/index.ts +++ b/packages/controller/index.ts @@ -1,17 +1,17 @@ -import { bootstrap } from "./controller"; -export { default as Controller } from "./src/Controller"; -export { default as ControllerRouter } from "./src/ControllerRouter"; -export { default as ControlConnection } from "./src/ControlConnection"; -export { default as HostConnection } from "./src/HostConnection"; -export { default as InstanceRecord } from "./src/InstanceRecord"; -export { default as InstanceManager } from "./src/InstanceManager"; -export { default as HostRecord } from "./src/HostRecord"; -export { default as BaseControllerPlugin } from "./src/BaseControllerPlugin"; -export { default as UserManager } from "./src/UserManager"; -export { default as UserRecord } from "./src/UserRecord"; -export { default as User } from "./src/User"; -export { default as WsServer } from "./src/WsServer"; +import { bootstrap } from "./controller.js"; +export { default as Controller } from "./src/Controller.js"; +export { default as ControllerRouter } from "./src/ControllerRouter.js"; +export { default as ControlConnection } from "./src/ControlConnection.js"; +export { default as HostConnection } from "./src/HostConnection.js"; +export { default as InstanceRecord } from "./src/InstanceRecord.js"; +export { default as InstanceManager } from "./src/InstanceManager.js"; +export { default as HostRecord } from "./src/HostRecord.js"; +export { default as BaseControllerPlugin } from "./src/BaseControllerPlugin.js"; +export { default as UserManager } from "./src/UserManager.js"; +export { default as UserRecord } from "./src/UserRecord.js"; +export { default as User } from "./src/User.js"; +export { default as WsServer } from "./src/WsServer.js"; -if (module === require.main) { +if (import.meta.main) { bootstrap(); } diff --git a/packages/controller/package.json b/packages/controller/package.json index cc070c83..0ff9fda0 100644 --- a/packages/controller/package.json +++ b/packages/controller/package.json @@ -22,6 +22,7 @@ "access": "public" }, + "type": "module", "main": "dist/node/index.js", "scripts": { "prepare": "tsc --build && webpack-cli --mode production", diff --git a/packages/controller/src/BaseConnection.ts b/packages/controller/src/BaseConnection.ts index 76949eb8..b04c1946 100644 --- a/packages/controller/src/BaseConnection.ts +++ b/packages/controller/src/BaseConnection.ts @@ -1,12 +1,12 @@ import path from "path"; import fs from "node:fs/promises"; -import type Controller from "./Controller"; -import type WsServerConnector from "./WsServerConnector"; +import type Controller from "./Controller.js"; +import type WsServerConnector from "./WsServerConnector.js"; import * as lib from "@clusterio/lib"; -const { logger } = lib; -import * as routes from "./routes"; +import { logger } from "@clusterio/lib"; +import * as routes from "./routes.js"; /** diff --git a/packages/controller/src/BaseControllerPlugin.ts b/packages/controller/src/BaseControllerPlugin.ts index 7b052b42..6f4c579c 100644 --- a/packages/controller/src/BaseControllerPlugin.ts +++ b/packages/controller/src/BaseControllerPlugin.ts @@ -2,10 +2,10 @@ import type { CollectorResult, Event, InstanceStatus, Logger, ModPack, ModInfo, PlayerEvent, PluginNodeEnvInfo, } from "@clusterio/lib"; -import type Controller from "./Controller"; -import type InstanceRecord from "./InstanceRecord"; -import type ControlConnection from "./ControlConnection"; -import type HostConnection from "./HostConnection"; +import type Controller from "./Controller.js"; +import type InstanceRecord from "./InstanceRecord.js"; +import type ControlConnection from "./ControlConnection.js"; +import type HostConnection from "./HostConnection.js"; /** * Base class for controller plugins diff --git a/packages/controller/src/ControlConnection.ts b/packages/controller/src/ControlConnection.ts index c9c9f8b0..63e2d667 100644 --- a/packages/controller/src/ControlConnection.ts +++ b/packages/controller/src/ControlConnection.ts @@ -1,15 +1,15 @@ -import type WsServerConnector from "./WsServerConnector"; +import type WsServerConnector from "./WsServerConnector.js"; import events from "events"; import * as lib from "@clusterio/lib"; -const { logFilter, logger } = lib; +import { logFilter, logger } from "@clusterio/lib"; -import BaseConnection from "./BaseConnection"; -import User from "./User"; -import UserRecord from "./UserRecord"; -import * as routes from "./routes"; -import Controller from "./Controller"; +import BaseConnection from "./BaseConnection.js"; +import User from "./User.js"; +import UserRecord from "./UserRecord.js"; +import * as routes from "./routes.js"; +import Controller from "./Controller.js"; const strcmp = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare; @@ -1325,5 +1325,3 @@ export default class ControlConnection extends BaseConnection { this._controller.debugEvents.on("message", this.ws_dumper); } } - -module.exports = ControlConnection; diff --git a/packages/controller/src/Controller.ts b/packages/controller/src/Controller.ts index d9362ff7..7c7aa18b 100644 --- a/packages/controller/src/Controller.ts +++ b/packages/controller/src/Controller.ts @@ -1,6 +1,6 @@ import type winston from "winston"; import { BlockList, type AddressInfo, isIPv4, isIPv6 } from "net"; -import type { ControllerArgs } from "../controller"; +import type { ControllerArgs } from "../controller.js"; import express, { type Request, type Response, type NextFunction, type Application } from "express"; import type { Static } from "@sinclair/typebox"; import finalhandler from "finalhandler"; @@ -14,22 +14,23 @@ import jwt from "jsonwebtoken"; import path from "path"; import semver from "semver"; import stream from "stream"; +import { fileURLToPath, pathToFileURL } from "node:url"; import * as lib from "@clusterio/lib"; -const { logger, Summary, Gauge } = lib; +import { logger, Summary, Gauge } from "@clusterio/lib"; -import HttpCloser from "./HttpCloser"; -import InstanceRecord from "./InstanceRecord"; -import * as metrics from "./metrics"; -import * as routes from "./routes"; -import User from "./User"; -import UserRecord from "./UserRecord"; -import UserManager from "./UserManager"; -import WsServer from "./WsServer"; -import HostRecord from "./HostRecord"; -import BaseControllerPlugin from "./BaseControllerPlugin"; -import ControllerRouter from "./ControllerRouter"; -import InstanceManager from "./InstanceManager"; +import HttpCloser from "./HttpCloser.js"; +import InstanceRecord from "./InstanceRecord.js"; +import * as metrics from "./metrics.js"; +import * as routes from "./routes.js"; +import User from "./User.js"; +import UserRecord from "./UserRecord.js"; +import UserManager from "./UserManager.js"; +import WsServer from "./WsServer.js"; +import HostRecord from "./HostRecord.js"; +import BaseControllerPlugin from "./BaseControllerPlugin.js"; +import ControllerRouter from "./ControllerRouter.js"; +import InstanceManager from "./InstanceManager.js"; const endpointDurationSummary = new Summary( "clusterio_controller_http_endpoint_duration_seconds", @@ -345,7 +346,7 @@ export default class Controller { // Start webpack development server if enabled if (args.dev || args.devPlugin) { - this._startDevServer(args); + await this._startDevServer(args); } this.config.on("fieldChanged", (field, curr, prev) => { @@ -383,7 +384,7 @@ export default class Controller { Controller.addAppRoutes(this.app, this.pluginInfos); if (!args.dev) { - let manifestPath = path.join(__dirname, "..", "..", "web", "manifest.json"); + let manifestPath = path.join(import.meta.dirname, "..", "..", "web", "manifest.json"); let manifest = await Controller.loadJsonObject(manifestPath); if (!manifest["main.js"]) { @@ -458,12 +459,13 @@ export default class Controller { async _startDevServer(args: ControllerArgs) { logger.warn("Webpack development mode enabled"); - const webpack = require("webpack"); - const webpackDevMiddleware = require("webpack-dev-middleware"); + const webpack = (await import("webpack")).default; + const webpackDevMiddleware = (await import("webpack-dev-middleware")).default; const webpackConfigs = []; if (args.dev) { - webpackConfigs.push(require("../../../webpack.config")({})); // Path outside of build + // @ts-expect-error this file is run from a different folder + webpackConfigs.push((await import("../../../webpack.config.cjs")).default({})); // Path outside of build } if (args.devPlugin) { let devPlugins = new Map(); @@ -472,7 +474,10 @@ export default class Controller { if (!info) { throw new lib.StartupError(`No plugin named ${name}`); } - let config = require(path.posix.join(info.requirePath, "webpack.config"))({}); + const webpackConfig = path.posix.join(info.requirePath, "webpack.config.cjs"); + let config = (await import( + path.isAbsolute(webpackConfig) ? pathToFileURL(webpackConfig).href : webpackConfig + )).default({}); devPlugins.set(name, webpackConfigs.length); webpackConfigs.push(config); } @@ -627,7 +632,9 @@ export default class Controller { try { // First check the clusterio version const runningVersion = this.config.get("controller.version"); - const packageJsonPath = require.resolve("@clusterio/controller/package.json"); + const packageJsonPath = fileURLToPath( + import.meta.resolve("@clusterio/controller/package.json"), + ); const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8")); if (runningVersion !== packageJson.version) { this.config.restartRequired = true; @@ -651,7 +658,7 @@ export default class Controller { } } } catch (err: any) { - logger.warn(`Failed to read package json for ${packageName}:\n${err.stack ?? err.message}`); + logger.warn(`Failed to read package.json for ${packageName}:\n${err.stack ?? err.message}`); } return false; @@ -660,7 +667,7 @@ export default class Controller { async checkRestartDowngrade() { try { const runningVersion = this.config.get("controller.version"); - const packageJsonPath = require.resolve("@clusterio/controller/package.json"); + const packageJsonPath = fileURLToPath(import.meta.resolve("@clusterio/controller/package.json")); const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8")); const installedVersion = packageJson.version; @@ -901,7 +908,7 @@ export default class Controller { // Set folder to serve static content from (the website) const staticOptions = { immutable: true, maxAge: 1000 * 86400 * 365 }; app.use("/static", - express.static(path.join(__dirname, "..", "..", "web", "static"), staticOptions) + express.static(path.join(import.meta.dirname, "..", "..", "web", "static"), staticOptions) ); app.use("/static", express.static("static", staticOptions)); // Used for data export files @@ -1124,7 +1131,7 @@ export default class Controller { for (let pluginInfo of this.pluginInfos) { try { let manifestPath = path.posix.join(pluginInfo.requirePath, "dist", "web", "manifest.json"); - pluginInfo.manifest = await Controller.loadJsonObject(require.resolve(manifestPath), true); + pluginInfo.manifest = await Controller.loadJsonObject(import.meta.resolve(manifestPath), true); } catch (err) { logger.warn(`Unable to load dist/web/manifest.json for plugin ${pluginInfo.name}`); } @@ -1241,7 +1248,10 @@ export default class Controller { } mainBundle = routes.stripStaticPrefix(mainBundle); - fs.readFile(path.join(__dirname, "..", "..", "..", "web", "index.html"), "utf8").then((content) => { + fs.readFile( + path.join(import.meta.dirname, "..", "..", "..", "web", "index.html"), + "utf8", + ).then((content) => { res.type("text/html"); res.send(content .replace(/__CLUSTER_NAME__/g, res.app.locals.controller.config.get("controller.name")) diff --git a/packages/controller/src/ControllerRouter.ts b/packages/controller/src/ControllerRouter.ts index 4072c941..4b90abb8 100644 --- a/packages/controller/src/ControllerRouter.ts +++ b/packages/controller/src/ControllerRouter.ts @@ -1,10 +1,10 @@ -import type Controller from "./Controller"; -import type BaseConnection from "./BaseConnection"; +import type Controller from "./Controller.js"; +import type BaseConnection from "./BaseConnection.js"; import * as lib from "@clusterio/lib"; -import HostConnection from "./HostConnection"; -import ControlConnection from "./ControlConnection"; -const { logger } = lib; +import { logger } from "@clusterio/lib"; +import HostConnection from "./HostConnection.js"; +import ControlConnection from "./ControlConnection.js"; export default class ControllerRouter { constructor( diff --git a/packages/controller/src/HostConnection.ts b/packages/controller/src/HostConnection.ts index e7132925..c368f8c8 100644 --- a/packages/controller/src/HostConnection.ts +++ b/packages/controller/src/HostConnection.ts @@ -1,12 +1,12 @@ -import type Controller from "./Controller"; -import type WsServerConnector from "./WsServerConnector"; +import type Controller from "./Controller.js"; +import type WsServerConnector from "./WsServerConnector.js"; import * as lib from "@clusterio/lib"; -const { logger } = lib; +import { logger } from "@clusterio/lib"; -import BaseConnection from "./BaseConnection"; -import InstanceRecord from "./InstanceRecord"; -import HostRecord from "./HostRecord"; +import BaseConnection from "./BaseConnection.js"; +import InstanceRecord from "./InstanceRecord.js"; +import HostRecord from "./HostRecord.js"; /** diff --git a/packages/controller/src/InstanceManager.ts b/packages/controller/src/InstanceManager.ts index e1cf1941..51292dc6 100644 --- a/packages/controller/src/InstanceManager.ts +++ b/packages/controller/src/InstanceManager.ts @@ -1,8 +1,8 @@ import * as lib from "@clusterio/lib"; -import type Controller from "./Controller"; -import type HostConnection from "./HostConnection"; -import InstanceRecord from "./InstanceRecord"; +import type Controller from "./Controller.js"; +import type HostConnection from "./HostConnection.js"; +import InstanceRecord from "./InstanceRecord.js"; export default class InstanceManager { private static readonly DefaultFactorioSettings = { diff --git a/packages/controller/src/User.ts b/packages/controller/src/User.ts index d82202a1..c4fe115f 100644 --- a/packages/controller/src/User.ts +++ b/packages/controller/src/User.ts @@ -4,7 +4,7 @@ import { PermissionError, permissions, } from "@clusterio/lib"; -import UserRecord from "./UserRecord"; +import UserRecord from "./UserRecord.js"; import { Static } from "@sinclair/typebox"; /** diff --git a/packages/controller/src/UserManager.ts b/packages/controller/src/UserManager.ts index 0b4861b8..60649e5f 100644 --- a/packages/controller/src/UserManager.ts +++ b/packages/controller/src/UserManager.ts @@ -3,8 +3,8 @@ import jwt from "jsonwebtoken"; import * as lib from "@clusterio/lib"; -import User from "./User"; -import UserRecord from "./UserRecord"; +import User from "./User.js"; +import UserRecord from "./UserRecord.js"; /** * Access layer for users on the controller. diff --git a/packages/controller/src/WsServer.ts b/packages/controller/src/WsServer.ts index c2b33c81..f6293b84 100644 --- a/packages/controller/src/WsServer.ts +++ b/packages/controller/src/WsServer.ts @@ -1,18 +1,18 @@ import type { IncomingMessage } from "http"; import type { Duplex } from "stream"; -import type Controller from "./Controller"; +import type Controller from "./Controller.js"; import jwt from "jsonwebtoken"; import WebSocket, { WebSocketServer } from "ws"; import * as lib from "@clusterio/lib"; -const { logger } = lib; +import { logger } from "@clusterio/lib"; -import ControlConnection from "./ControlConnection"; -import HostConnection from "./HostConnection"; -import WsServerConnector from "./WsServerConnector"; +import ControlConnection from "./ControlConnection.js"; +import HostConnection from "./HostConnection.js"; +import WsServerConnector from "./WsServerConnector.js"; -import { version as packageVersion } from "../package.json"; +import packageConfig from "../package.json" with { type: "json" }; const wsMessageCounter = new lib.Counter( "clusterio_controller_websocket_message_total", @@ -45,12 +45,12 @@ export default class WsServer { sessionAud: string = `session-${Date.now()}`; nextSessionId: number = 1; nextControlId: number = 1; - wss: WebSocket.Server; + wss: WebSocketServer; constructor(controller: Controller) { this.controller = controller; - this.wss = new WebSocket.Server({ + this.wss = new WebSocketServer({ noServer: true, path: "/api/socket", }); @@ -139,7 +139,7 @@ export default class WsServer { loadedPlugins[name] = plugin.info.version; } - socket.send(JSON.stringify(new lib.MessageHello(new lib.HelloData(packageVersion, loadedPlugins)))); + socket.send(JSON.stringify(new lib.MessageHello(new lib.HelloData(packageConfig.version, loadedPlugins)))); this.attachHandler(socket, req); } @@ -287,10 +287,11 @@ ${err.stack}` logger.info( `WsServer | registered host ${data.id} using version ${data.version}` ); - if (data.version !== packageVersion) { + if (data.version !== packageConfig.version) { logger.warn( `Host ${data.id} connected using version ${data.version} which does not match the ` + - `version of the controller is currently running (${packageVersion}). It may not work as expected.` + `version of the controller is currently running (${packageConfig.version}). ` + + "It may not work as expected." ); } diff --git a/packages/controller/src/WsServerConnector.ts b/packages/controller/src/WsServerConnector.ts index 403feeb6..7277f8c3 100644 --- a/packages/controller/src/WsServerConnector.ts +++ b/packages/controller/src/WsServerConnector.ts @@ -1,10 +1,10 @@ -import type WebSocket from "ws"; +import { type WebSocket } from "ws"; import events from "events"; import assert from "assert/strict"; import * as lib from "@clusterio/lib"; -const { logger } = lib; +import { logger } from "@clusterio/lib"; /** @@ -49,7 +49,7 @@ export default class WsServerConnector extends lib.WebSocketBaseConnector { * @param account - account data to provide to control connection */ ready( - socket: WebSocket.WebSocket, + socket: WebSocket, src: lib.Address, sessionToken: string, account: lib.AccountDetails | undefined @@ -80,7 +80,7 @@ export default class WsServerConnector extends lib.WebSocketBaseConnector { * @param lastSeq - The last message the client received. */ continue( - socket: WebSocket.WebSocket, + socket: WebSocket, lastSeq: number ) { diff --git a/packages/controller/src/routes.ts b/packages/controller/src/routes.ts index f99d729f..5e522583 100644 --- a/packages/controller/src/routes.ts +++ b/packages/controller/src/routes.ts @@ -1,5 +1,5 @@ import type { Application, Request, Response } from "express"; -import Controller from "./Controller"; +import Controller from "./Controller.js"; import busboy from "busboy"; import crypto from "crypto"; @@ -12,7 +12,7 @@ import nodeStream from "stream"; import util from "util"; import * as lib from "@clusterio/lib"; -const { logger } = lib; +import { logger } from "@clusterio/lib"; const finished = util.promisify(nodeStream.finished); diff --git a/packages/controller/webpack.config.js b/packages/controller/webpack.config.cjs similarity index 100% rename from packages/controller/webpack.config.js rename to packages/controller/webpack.config.cjs diff --git a/packages/create/create.js b/packages/create/create.js index 1fc26b6f..457dae2b 100755 --- a/packages/create/create.js +++ b/packages/create/create.js @@ -1,20 +1,19 @@ #!/usr/bin/env node -"use strict"; -const child_process = require("child_process"); -const fs = require("node:fs/promises"); -const inquirer = require("inquirer"); -const os = require("os"); -const path = require("path"); -const stream = require("stream"); -const util = require("util"); -const yargs = require("yargs"); +import child_process from "node:child_process"; +import fs from "node:fs/promises"; +import inquirer from "inquirer"; +import os from "node:os"; +import path from "node:path"; +import stream from "node:stream"; +import util from "node:util"; +import yargs from "yargs"; -const { levels, logger, setLogLevel } = require("./logging"); -const { copyPluginTemplates } = require("./template"); +import { levels, logger, setLogLevel } from "./logging.js"; +import { copyPluginTemplates } from "./template.js"; let dev = false; const scriptExt = process.platform === "win32" ? ".cmd" : ""; -const { escapeArg } = require("./escape_arg"); +import { escapeArg } from "./escape_arg.js"; // I hate this, there is a bug with rl.close on windows that was meant to have been fixed in Node14.4 // See nodejs/node#21771 and nodejs/node#30701 for the apparent fix that was applied @@ -147,21 +146,21 @@ async function execFile(cmd, args) { async function execController(args) { if (dev) { - return await execFile("node", [path.join(__dirname, "..", "controller"), ...args]); + return await execFile("node", [path.join(import.meta.dirname, "..", "controller"), ...args]); } return await execFile(path.join("node_modules", ".bin", `clusteriocontroller${scriptExt}`), args); } async function execHost(args) { if (dev) { - return await execFile("node", [path.join(__dirname, "..", "host"), ...args]); + return await execFile("node", [path.join(import.meta.dirname, "..", "host"), ...args]); } return await execFile(path.join("node_modules", ".bin", `clusteriohost${scriptExt}`), args); } async function execCtl(args) { if (dev) { - return await execFile("node", [path.join(__dirname, "..", "ctl"), ...args]); + return await execFile("node", [path.join(import.meta.dirname, "..", "ctl"), ...args]); } return await execFile(path.join("node_modules", ".bin", `clusterioctl${scriptExt}`), args); } @@ -408,7 +407,7 @@ async function installClusterio(mode, plugins) { for (let plugin of plugins) { if (!pluginList.has(plugin)) { - let pluginInfo = require(require.resolve(plugin, { paths: [process.cwd()] })).plugin; + let pluginInfo = (await import(plugin)).plugin; pluginList.set(pluginInfo.name, plugin); } } @@ -900,7 +899,7 @@ async function main() { /* eslint-enable no-console */ } -if (module === require.main) { +if (import.meta.main) { main().catch(err => { if (err instanceof InstallError) { logger.error(err.message); diff --git a/packages/create/escape_arg.js b/packages/create/escape_arg.js index 3767ae82..b194c75f 100644 --- a/packages/create/escape_arg.js +++ b/packages/create/escape_arg.js @@ -1,6 +1,4 @@ -"use strict"; - -function escapeArg(arg) { +export function escapeArg(arg) { // When shell: true is used execFile does not escaping of arguments, they are // simply joined by space and then passed to the shell. @@ -56,7 +54,3 @@ function escapeArg(arg) { } return `"${arg.replace(/[$`"\\]/g, "\\$&")}"`; } - -module.exports = { - escapeArg, -}; diff --git a/packages/create/logging.js b/packages/create/logging.js index 265dd8e1..9954b666 100644 --- a/packages/create/logging.js +++ b/packages/create/logging.js @@ -1,8 +1,7 @@ /* eslint-disable no-console */ -"use strict"; -const chalk = require("chalk"); +import chalk from "chalk"; -const levels = Object.freeze({ +export const levels = Object.freeze({ fatal: 0, error: 1, warn: 2, @@ -11,7 +10,7 @@ const levels = Object.freeze({ verbose: 6, }); -const colors = { +export const colors = { fatal: "inverse red", error: "bold redBright", warn: "bold yellowBright", @@ -21,7 +20,7 @@ const colors = { }; let maxLogLevel = levels.info; -const logger = {}; +export const logger = {}; for (let [name, level] of Object.entries(levels)) { // eslint-disable-next-line no-loop-func logger[name] = message => { @@ -42,10 +41,6 @@ for (let [name, level] of Object.entries(levels)) { }; } -module.exports = { - colors, - levels, - setLogLevel: (level) => { maxLogLevel = level; }, - logger, -}; - +export function setLogLevel(level) { + maxLogLevel = level; +} diff --git a/packages/create/package.json b/packages/create/package.json index aa0bfd24..601defa0 100644 --- a/packages/create/package.json +++ b/packages/create/package.json @@ -22,6 +22,7 @@ "access": "public" }, + "type": "module", "main": "create.js", "bin": { "clusteriocreate": "./create.js" diff --git a/packages/create/template.js b/packages/create/template.js index e878a42a..99505d70 100644 --- a/packages/create/template.js +++ b/packages/create/template.js @@ -1,9 +1,8 @@ -"use strict"; -const { logger } = require("./logging"); -const fs = require("node:fs/promises"); -const path = require("path"); +import { logger } from "./logging.js"; +import fs from "node:fs/promises"; +import path from "node:path"; -const _package = require("./package.json"); +import _package from "./package.json" with { type: "json" }; async function copyTemplateFile(src, dst, properties) { logger.verbose(`Writing ${dst} from template ${src}`); @@ -79,7 +78,7 @@ async function copyTemplateFile(src, dst, properties) { } } -async function copyPluginTemplates(pluginName, templates) { +export async function copyPluginTemplates(pluginName, templates) { logger.info(`Please wait, coping templates for ${templates.join(", ")}`); const files = new Map(); const prepare = []; @@ -96,8 +95,11 @@ async function copyPluginTemplates(pluginName, templates) { // Get the file extension and path to the templates const ext = javascriptOnly ? "js" : "ts"; - const templatePath = path.resolve(__dirname, javascriptOnly ? "./templates/plugin-js" : "./templates/plugin-ts"); - const commonPath = path.resolve(__dirname, "./templates/common"); + const templatePath = path.resolve( + import.meta.dirname, + javascriptOnly ? "./templates/plugin-js" : "./templates/plugin-ts" + ); + const commonPath = path.resolve(import.meta.dirname, "./templates/common"); // Files included in all templates files.set(".gitignore", path.join(commonPath, "template.gitignore")); @@ -119,7 +121,7 @@ async function copyPluginTemplates(pluginName, templates) { // Files and dependences to support webpack if (webpack) { prepare.push("webpack-cli --mode production"); - files.set("webpack.config.js", path.join(commonPath, "webpack.config.js")); + files.set("webpack.config.cjs", path.join(commonPath, "webpack.config.cjs")); if (templates.includes("web")) { files.set(`web/index.${ext}x`, path.join(templatePath, `web/plugin.${ext}x`)); pluginContexts += 1; @@ -202,7 +204,3 @@ async function copyPluginTemplates(pluginName, templates) { await Promise.all(writes); logger.info("Successfully wrote all template files"); } - -module.exports = { - copyPluginTemplates, -}; diff --git a/packages/create/templates/common/package.json b/packages/create/templates/common/package.json index 0e01b9d7..7169e2aa 100644 --- a/packages/create/templates/common/package.json +++ b/packages/create/templates/common/package.json @@ -2,6 +2,7 @@ "name": "__plugin_name__", "version": "0.1.0", "description": "Example Description. Package. Change me in package.json", + "type": "module", //%if typescript "main": "dist/node/index.js", //%endif diff --git a/packages/create/templates/common/webpack.config.js b/packages/create/templates/common/webpack.config.cjs similarity index 100% rename from packages/create/templates/common/webpack.config.js rename to packages/create/templates/common/webpack.config.cjs diff --git a/packages/create/templates/plugin-js/controller.js b/packages/create/templates/plugin-js/controller.js index fae78fdc..4efb047d 100644 --- a/packages/create/templates/plugin-js/controller.js +++ b/packages/create/templates/plugin-js/controller.js @@ -1,19 +1,18 @@ -"use strict"; -const lib = require("@clusterio/lib"); -const { BaseControllerPlugin } = require("@clusterio/controller"); +import * as lib from "@clusterio/lib"; +import { BaseControllerPlugin } from "@clusterio/controller"; //%if multi_context // Messages requires multi context -const { +import { PluginExampleEvent, PluginExampleRequest, //%endif //%if controller & web // Subscribing requires web content and the controller ExampleSubscribableUpdate, ExampleSubscribableValue, //%endif //%if multi_context // Messages requires multi context -} = require("./messages"); +} from "./messages.js"; //%endif -class ControllerPlugin extends BaseControllerPlugin { +export class ControllerPlugin extends BaseControllerPlugin { //%if controller & web // Subscribing requires web content and the controller exampleDatabase; storageDirty = false; @@ -79,7 +78,3 @@ class ControllerPlugin extends BaseControllerPlugin { } //%endif } - -module.exports = { - ControllerPlugin, -}; diff --git a/packages/create/templates/plugin-js/ctl.js b/packages/create/templates/plugin-js/ctl.js index 95e886b9..03377b92 100644 --- a/packages/create/templates/plugin-js/ctl.js +++ b/packages/create/templates/plugin-js/ctl.js @@ -1,8 +1,7 @@ -"use strict"; -const { BaseCtlPlugin } = require("@clusterio/ctl"); -const { CommandTree, Command } = require("@clusterio/lib"); +import { BaseCtlPlugin } from "@clusterio/ctl"; +import { CommandTree, Command } from "@clusterio/lib"; //%// We do not check for multi context here because it doesn't make sense to have a ctl without messages -const { PluginExampleEvent, PluginExampleRequest } = require("./messages"); +import { PluginExampleEvent, PluginExampleRequest } from "./messages.js"; /* eslint-disable no-console */ const pluginCommands = new CommandTree({ @@ -21,12 +20,8 @@ pluginCommands.add(new Command({ })); /* eslint-enable no-console */ -class CtlPlugin extends BaseCtlPlugin { +export class CtlPlugin extends BaseCtlPlugin { async addCommands(rootCommand) { rootCommand.add(pluginCommands); } } - -module.exports = { - CtlPlugin, -}; diff --git a/packages/create/templates/plugin-js/host.js b/packages/create/templates/plugin-js/host.js index c1e1468d..6675acff 100644 --- a/packages/create/templates/plugin-js/host.js +++ b/packages/create/templates/plugin-js/host.js @@ -1,11 +1,10 @@ -"use strict"; -const lib = require("@clusterio/lib"); -const { BaseHostPlugin } = require("@clusterio/host"); +import * as lib from "@clusterio/lib"; +import { BaseHostPlugin } from "@clusterio/host"; //%if multi_context -const { PluginExampleEvent, PluginExampleRequest } = require("./messages"); +import { PluginExampleEvent, PluginExampleRequest } from "./messages.js"; //%endif -class HostPlugin extends BaseHostPlugin { +export class HostPlugin extends BaseHostPlugin { //%if multi_context async init() { this.host.handle(PluginExampleEvent, this.handlePluginExampleEvent.bind(this)); @@ -35,7 +34,3 @@ class HostPlugin extends BaseHostPlugin { } //%endif } - -module.exports = { - HostPlugin, -}; diff --git a/packages/create/templates/plugin-js/index.js b/packages/create/templates/plugin-js/index.js index d773d4c4..932b460d 100644 --- a/packages/create/templates/plugin-js/index.js +++ b/packages/create/templates/plugin-js/index.js @@ -1,7 +1,6 @@ -"use strict"; -const lib = require("@clusterio/lib"); +import * as lib from "@clusterio/lib"; //%if multi_context -const Messages = require("./messages"); +import Messages from "./messages.js"; lib.definePermission({ name: "__plugin_name__.example.permission.event", @@ -36,7 +35,7 @@ lib.definePermission({ }); //%endif -const plugin = { +export const plugin = { name: "__plugin_name__", title: "__plugin_name__", description: "Example Description. Plugin. Change me in index.js", @@ -44,7 +43,7 @@ const plugin = { //%endif //%if controller - controllerEntrypoint: "./dist/node/controller", + controllerEntrypoint: "./dist/node/controller.js", //%endif //%if controller & config controllerConfigFields: { @@ -60,7 +59,7 @@ const plugin = { //%endif //%if host - hostEntrypoint: "./dist/node/host", + hostEntrypoint: "./dist/node/host.js", //%endif //%if host & config hostConfigFields: { @@ -76,7 +75,7 @@ const plugin = { //%endif //%if instance | module // Modules load an empty instance plugin - instanceEntrypoint: "./dist/node/instance", + instanceEntrypoint: "./dist/node/instance.js", //%endif //%if instance & config instanceConfigFields: { @@ -92,7 +91,7 @@ const plugin = { //%endif //%if ctl - ctlEntrypoint: "./dist/node/ctl", + ctlEntrypoint: "./dist/node/ctl.js", //%endif //%if ctl & config controlConfigFields: { @@ -124,7 +123,3 @@ const plugin = { ], //%endif }; - -module.exports = { - plugin, -}; diff --git a/packages/create/templates/plugin-js/instance.js b/packages/create/templates/plugin-js/instance.js index 41175938..8049e468 100644 --- a/packages/create/templates/plugin-js/instance.js +++ b/packages/create/templates/plugin-js/instance.js @@ -1,11 +1,10 @@ -"use strict"; -const lib = require("@clusterio/lib"); -const { BaseInstancePlugin } = require("@clusterio/host"); +import * as lib from "@clusterio/lib"; +import { BaseInstancePlugin } from "@clusterio/host"; //%if multi_context -const { PluginExampleEvent, PluginExampleRequest } = require("./messages"); +import { PluginExampleEvent, PluginExampleRequest } from "./messages.js"; //%endif -class InstancePlugin extends BaseInstancePlugin { +export class InstancePlugin extends BaseInstancePlugin { //%if multi_context | module async init() { //%if multi_context @@ -68,7 +67,3 @@ class InstancePlugin extends BaseInstancePlugin { } //%endif } - -module.exports = { - InstancePlugin, -}; diff --git a/packages/create/templates/plugin-js/instance_empty.js b/packages/create/templates/plugin-js/instance_empty.js index 177f9952..cf4f0b68 100644 --- a/packages/create/templates/plugin-js/instance_empty.js +++ b/packages/create/templates/plugin-js/instance_empty.js @@ -1,11 +1,6 @@ -"use strict"; -const { BaseInstancePlugin } = require("@clusterio/host"); +import { BaseInstancePlugin } from "@clusterio/host"; -class InstancePlugin extends BaseInstancePlugin { +export class InstancePlugin extends BaseInstancePlugin { // This class is empty because an instance plugin must be defined for a module to be injected // This requirement may change in the future to allow for standalone modules } - -module.exports = { - InstancePlugin, -}; diff --git a/packages/create/templates/plugin-js/messages.js b/packages/create/templates/plugin-js/messages.js index aaffd9f6..296856ea 100644 --- a/packages/create/templates/plugin-js/messages.js +++ b/packages/create/templates/plugin-js/messages.js @@ -1,8 +1,7 @@ -"use strict"; -const { plainJson, jsonArray, JsonBoolean, JsonNumber, JsonString, StringEnum } = require("@clusterio/lib"); -const { Type } = require("@sinclair/typebox"); +import { plainJson, jsonArray, JsonBoolean, JsonNumber, JsonString, StringEnum } from "@clusterio/lib"; +import { Type } from "@sinclair/typebox"; -class PluginExampleEvent { +export class PluginExampleEvent { static type = "event"; static src = ["host", "control"]; static dst = ["controller", "host", "instance"]; @@ -24,7 +23,7 @@ class PluginExampleEvent { } } -class PluginExampleRequest { +export class PluginExampleRequest { static type = "request"; static src = ["host", "control"]; static dst = ["controller", "host", "instance"]; @@ -52,7 +51,7 @@ class PluginExampleRequest { } //%if controller & web // Subscribing requires web content and the controller -class ExampleSubscribableValue { +export class ExampleSubscribableValue { constructor(id, updatedAtMs, isDeleted) { this.id = id; this.updatedAtMs = updatedAtMs; @@ -70,7 +69,7 @@ class ExampleSubscribableValue { } } -class ExampleSubscribableUpdate { +export class ExampleSubscribableUpdate { static type = "event"; static src = "controller"; static dst = "control"; @@ -90,12 +89,3 @@ class ExampleSubscribableUpdate { } } //%endif - -module.exports = { - PluginExampleEvent, - PluginExampleRequest, -//%if controller & web // Subscribing requires web content and the controller - ExampleSubscribableValue, - ExampleSubscribableUpdate, -//%endif -}; diff --git a/packages/create/templates/plugin-js/web/plugin.jsx b/packages/create/templates/plugin-js/web/plugin.jsx index 4aa14c85..48f90cf9 100644 --- a/packages/create/templates/plugin-js/web/plugin.jsx +++ b/packages/create/templates/plugin-js/web/plugin.jsx @@ -21,7 +21,7 @@ import { ExampleSubscribableUpdate, ExampleSubscribableValue, //%endif //%if multi_context // Messages requires multi context -} from "../messages"; +} from "../messages.js"; //%endif import * as lib from "@clusterio/lib"; diff --git a/packages/create/templates/plugin-ts/controller.ts b/packages/create/templates/plugin-ts/controller.ts index 59fb296d..f69e5b72 100644 --- a/packages/create/templates/plugin-ts/controller.ts +++ b/packages/create/templates/plugin-ts/controller.ts @@ -9,7 +9,7 @@ import { ExampleSubscribableUpdate, ExampleSubscribableValue, //%endif //%if multi_context // Messages requires multi context -} from "./messages"; +} from "./messages.js"; //%endif export class ControllerPlugin extends BaseControllerPlugin { diff --git a/packages/create/templates/plugin-ts/ctl.ts b/packages/create/templates/plugin-ts/ctl.ts index dc0fa01b..0a42aa8a 100644 --- a/packages/create/templates/plugin-ts/ctl.ts +++ b/packages/create/templates/plugin-ts/ctl.ts @@ -1,7 +1,7 @@ import { BaseCtlPlugin, type Control } from "@clusterio/ctl"; import { CommandTree, Command } from "@clusterio/lib"; //%// We do not check for multi context here because it doesn't make sense to have a ctl without messages -import { PluginExampleEvent, PluginExampleRequest } from "./messages"; +import { PluginExampleEvent, PluginExampleRequest } from "./messages.js"; /* eslint-disable no-console */ const pluginCommands = new CommandTree({ diff --git a/packages/create/templates/plugin-ts/host.ts b/packages/create/templates/plugin-ts/host.ts index 9a395988..ed31b4e5 100644 --- a/packages/create/templates/plugin-ts/host.ts +++ b/packages/create/templates/plugin-ts/host.ts @@ -1,7 +1,7 @@ import * as lib from "@clusterio/lib"; import { BaseHostPlugin } from "@clusterio/host"; //%if multi_context -import { PluginExampleEvent, PluginExampleRequest } from "./messages"; +import { PluginExampleEvent, PluginExampleRequest } from "./messages.js"; //%endif export class HostPlugin extends BaseHostPlugin { diff --git a/packages/create/templates/plugin-ts/index.ts b/packages/create/templates/plugin-ts/index.ts index 539a66bc..8027808b 100644 --- a/packages/create/templates/plugin-ts/index.ts +++ b/packages/create/templates/plugin-ts/index.ts @@ -1,6 +1,6 @@ import * as lib from "@clusterio/lib"; //%if multi_context -import * as Messages from "./messages"; +import * as Messages from "./messages.js"; lib.definePermission({ name: "__plugin_name__.example.permission.event", @@ -71,7 +71,7 @@ export const plugin: lib.PluginDeclaration = { //%endif //%if controller - controllerEntrypoint: "./dist/node/controller", + controllerEntrypoint: "./dist/node/controller.js", //%endif //%if controller & config controllerConfigFields: { @@ -87,7 +87,7 @@ export const plugin: lib.PluginDeclaration = { //%endif //%if host - hostEntrypoint: "./dist/node/host", + hostEntrypoint: "./dist/node/host.js", //%endif //%if host & config hostConfigFields: { @@ -103,7 +103,7 @@ export const plugin: lib.PluginDeclaration = { //%endif //%if instance | module // Modules load an empty instance plugin - instanceEntrypoint: "./dist/node/instance", + instanceEntrypoint: "./dist/node/instance.js", //%endif //%if instance & config instanceConfigFields: { @@ -119,7 +119,7 @@ export const plugin: lib.PluginDeclaration = { //%endif //%if ctl - ctlEntrypoint: "./dist/node/ctl", + ctlEntrypoint: "./dist/node/ctl.js", //%endif //%if ctl & config controlConfigFields: { diff --git a/packages/create/templates/plugin-ts/instance.ts b/packages/create/templates/plugin-ts/instance.ts index 133efa6f..9f3cf81d 100644 --- a/packages/create/templates/plugin-ts/instance.ts +++ b/packages/create/templates/plugin-ts/instance.ts @@ -1,7 +1,7 @@ import * as lib from "@clusterio/lib"; import { BaseInstancePlugin } from "@clusterio/host"; //%if multi_context -import { PluginExampleEvent, PluginExampleRequest } from "./messages"; +import { PluginExampleEvent, PluginExampleRequest } from "./messages.js"; //%endif //%if module diff --git a/packages/create/templates/plugin-ts/web/plugin.tsx b/packages/create/templates/plugin-ts/web/plugin.tsx index 40d1ca9b..3584a098 100644 --- a/packages/create/templates/plugin-ts/web/plugin.tsx +++ b/packages/create/templates/plugin-ts/web/plugin.tsx @@ -21,7 +21,7 @@ import { ExampleSubscribableUpdate, ExampleSubscribableValue, //%endif //%if multi_context // Messages requires multi context -} from "../messages"; +} from "../messages.js"; //%endif import * as lib from "@clusterio/lib"; diff --git a/packages/ctl/ctl.ts b/packages/ctl/ctl.ts index 926db7e0..b3141d5f 100755 --- a/packages/ctl/ctl.ts +++ b/packages/ctl/ctl.ts @@ -7,16 +7,16 @@ import fs from "node:fs/promises"; import yargs, { type Argv } from "yargs"; import path from "path"; -import { version } from "./package.json"; +import packageConfig from "./package.json" with { type: "json" }; import { strict as assert } from "assert"; // Reduce startup time by lazy compiling schemas. -(global as any).lazySchemaCompilation = true; +import "./src/set_lazy_schema_compliation.js"; import * as lib from "@clusterio/lib"; import { ConsoleTransport, levels, logger } from "@clusterio/lib"; -import * as commands from "./src/commands"; -import BaseCtlPlugin from "./src/BaseCtlPlugin"; +import * as commands from "./src/commands.js"; +import BaseCtlPlugin from "./src/BaseCtlPlugin.js"; /** @@ -37,7 +37,7 @@ class ControlConnector extends lib.WebSocketClientConnector { new lib.MessageRegisterControl( new lib.RegisterControlData( this._token, - version, + packageConfig.version, ) ) ); @@ -377,6 +377,6 @@ ${err.stack}` }); } -if (module === require.main) { +if (import.meta.main) { bootstrap(); } diff --git a/packages/ctl/index.ts b/packages/ctl/index.ts index 54bd5f27..0852c19b 100644 --- a/packages/ctl/index.ts +++ b/packages/ctl/index.ts @@ -1,7 +1,7 @@ -import { bootstrap } from "./ctl"; -export * from "./ctl"; -export { default as BaseCtlPlugin } from "./src/BaseCtlPlugin"; +import { bootstrap } from "./ctl.js"; +export * from "./ctl.js"; +export { default as BaseCtlPlugin } from "./src/BaseCtlPlugin.js"; -if (module === require.main) { +if (import.meta.main) { bootstrap(); } diff --git a/packages/ctl/package.json b/packages/ctl/package.json index 96d18c33..04f1373b 100644 --- a/packages/ctl/package.json +++ b/packages/ctl/package.json @@ -22,6 +22,7 @@ "access": "public" }, + "type": "module", "main": "dist/node/index.js", "scripts": { "prepare": "tsc --build", diff --git a/packages/ctl/src/commands.ts b/packages/ctl/src/commands.ts index ffb46193..5e31a507 100644 --- a/packages/ctl/src/commands.ts +++ b/packages/ctl/src/commands.ts @@ -1,17 +1,17 @@ import type { Argv } from "yargs"; import * as lib from "@clusterio/lib"; -import type BaseCtlPlugin from "./BaseCtlPlugin"; -import { controllerCommands } from "./commands_controller"; -import { hostCommands } from "./commands_host"; -import { instanceCommands } from "./commands_instance"; -import { modPackCommands } from "./commands_mod_pack"; -import { modCommands } from "./commands_mod"; -import { permissionCommands } from "./commands_permission"; -import { roleCommands } from "./commands_role"; -import { userCommands } from "./commands_user"; -import { logCommands } from "./commands_log"; -import { debugCommands } from "./commands_debug"; +import type BaseCtlPlugin from "./BaseCtlPlugin.js"; +import { controllerCommands } from "./commands_controller.js"; +import { hostCommands } from "./commands_host.js"; +import { instanceCommands } from "./commands_instance.js"; +import { modPackCommands } from "./commands_mod_pack.js"; +import { modCommands } from "./commands_mod.js"; +import { permissionCommands } from "./commands_permission.js"; +import { roleCommands } from "./commands_role.js"; +import { userCommands } from "./commands_user.js"; +import { logCommands } from "./commands_log.js"; +import { debugCommands } from "./commands_debug.js"; export async function registerCommands(ctlPlugins: Map, yargs: Argv) { const rootCommands = new lib.CommandTree({ name: "clusterioctl", description: "Manage cluster" }); diff --git a/packages/ctl/src/commands_controller.ts b/packages/ctl/src/commands_controller.ts index fbdde7a4..4bfbd41e 100644 --- a/packages/ctl/src/commands_controller.ts +++ b/packages/ctl/src/commands_controller.ts @@ -5,9 +5,9 @@ import os from "os"; import child_process from "child_process"; import * as lib from "@clusterio/lib"; -import type { Control } from "../ctl"; -import { print } from "./command_ops"; -import { serializedConfigToString, getEditor, configToKeyVal } from "./config_ops"; +import type { Control } from "../ctl.js"; +import { print } from "./command_ops.js"; +import { serializedConfigToString, getEditor, configToKeyVal } from "./config_ops.js"; const asTable = asTableModule.configure({ delimiter: " | " }); diff --git a/packages/ctl/src/commands_debug.ts b/packages/ctl/src/commands_debug.ts index af991afc..c69e9298 100644 --- a/packages/ctl/src/commands_debug.ts +++ b/packages/ctl/src/commands_debug.ts @@ -1,5 +1,5 @@ import * as lib from "@clusterio/lib"; -import type { Control } from "../ctl"; +import type { Control } from "../ctl.js"; export const debugCommands = new lib.CommandTree({ name: "debug", description: "Debugging utilities" }); debugCommands.add(new lib.Command({ diff --git a/packages/ctl/src/commands_host.ts b/packages/ctl/src/commands_host.ts index c44db2fe..fc269e3b 100644 --- a/packages/ctl/src/commands_host.ts +++ b/packages/ctl/src/commands_host.ts @@ -6,9 +6,9 @@ import child_process from "child_process"; import * as lib from "@clusterio/lib"; import { logger } from "@clusterio/lib"; -import type { Control } from "../ctl"; -import { print } from "./command_ops"; -import { serializedConfigToString, getEditor, configToKeyVal } from "./config_ops"; +import type { Control } from "../ctl.js"; +import { print } from "./command_ops.js"; +import { serializedConfigToString, getEditor, configToKeyVal } from "./config_ops.js"; const asTable = asTableModule.configure({ delimiter: " | " }); diff --git a/packages/ctl/src/commands_instance.ts b/packages/ctl/src/commands_instance.ts index 173451b4..8451d56c 100644 --- a/packages/ctl/src/commands_instance.ts +++ b/packages/ctl/src/commands_instance.ts @@ -9,9 +9,9 @@ import util from "util"; import * as lib from "@clusterio/lib"; import { logger } from "@clusterio/lib"; -import type { Control } from "../ctl"; -import { print } from "./command_ops"; -import { serializedConfigToString, getEditor, configToKeyVal } from "./config_ops"; +import type { Control } from "../ctl.js"; +import { print } from "./command_ops.js"; +import { serializedConfigToString, getEditor, configToKeyVal } from "./config_ops.js"; const asTable = asTableModule.configure({ delimiter: " | " }); const finished = util.promisify(stream.finished); diff --git a/packages/ctl/src/commands_log.ts b/packages/ctl/src/commands_log.ts index c121b3bc..4cce13a3 100644 --- a/packages/ctl/src/commands_log.ts +++ b/packages/ctl/src/commands_log.ts @@ -2,7 +2,7 @@ import winston from "winston"; import * as lib from "@clusterio/lib"; import { ConsoleTransport, levels, logger } from "@clusterio/lib"; -import type { Control } from "../ctl"; +import type { Control } from "../ctl.js"; export const logCommands = new lib.CommandTree({ name: "log", description: "Log inspection" }); logCommands.add(new lib.Command({ diff --git a/packages/ctl/src/commands_mod.ts b/packages/ctl/src/commands_mod.ts index 83f8873a..778ead86 100644 --- a/packages/ctl/src/commands_mod.ts +++ b/packages/ctl/src/commands_mod.ts @@ -7,8 +7,8 @@ import util from "util"; import * as lib from "@clusterio/lib"; import { logger } from "@clusterio/lib"; -import type { Control } from "../ctl"; -import { print } from "./command_ops"; +import type { Control } from "../ctl.js"; +import { print } from "./command_ops.js"; const asTable = asTableModule.configure({ delimiter: " | " }); const finished = util.promisify(stream.finished); diff --git a/packages/ctl/src/commands_mod_pack.ts b/packages/ctl/src/commands_mod_pack.ts index a4af8a90..6c88679f 100644 --- a/packages/ctl/src/commands_mod_pack.ts +++ b/packages/ctl/src/commands_mod_pack.ts @@ -2,8 +2,8 @@ import asTableModule from "as-table"; import * as lib from "@clusterio/lib"; import { logger } from "@clusterio/lib"; -import type { Control } from "../ctl"; -import { print } from "./command_ops"; +import type { Control } from "../ctl.js"; +import { print } from "./command_ops.js"; const asTable = asTableModule.configure({ delimiter: " | " }); diff --git a/packages/ctl/src/commands_permission.ts b/packages/ctl/src/commands_permission.ts index 94d2fbe4..0094314b 100644 --- a/packages/ctl/src/commands_permission.ts +++ b/packages/ctl/src/commands_permission.ts @@ -1,8 +1,8 @@ import asTableModule from "as-table"; import * as lib from "@clusterio/lib"; -import type { Control } from "../ctl"; -import { print } from "./command_ops"; +import type { Control } from "../ctl.js"; +import { print } from "./command_ops.js"; const asTable = asTableModule.configure({ delimiter: " | " }); diff --git a/packages/ctl/src/commands_role.ts b/packages/ctl/src/commands_role.ts index ae99188d..e1707255 100644 --- a/packages/ctl/src/commands_role.ts +++ b/packages/ctl/src/commands_role.ts @@ -2,8 +2,8 @@ import asTableModule from "as-table"; import * as lib from "@clusterio/lib"; import { logger } from "@clusterio/lib"; -import type { Control } from "../ctl"; -import { print } from "./command_ops"; +import type { Control } from "../ctl.js"; +import { print } from "./command_ops.js"; const asTable = asTableModule.configure({ delimiter: " | " }); diff --git a/packages/ctl/src/commands_user.ts b/packages/ctl/src/commands_user.ts index 37887013..1f8817f6 100644 --- a/packages/ctl/src/commands_user.ts +++ b/packages/ctl/src/commands_user.ts @@ -3,8 +3,8 @@ import asTableModule from "as-table"; import path from "path"; import * as lib from "@clusterio/lib"; -import type { Control } from "../ctl"; -import { print } from "./command_ops"; +import type { Control } from "../ctl.js"; +import { print } from "./command_ops.js"; const asTable = asTableModule.configure({ delimiter: " | " }); diff --git a/packages/ctl/src/set_lazy_schema_compliation.ts b/packages/ctl/src/set_lazy_schema_compliation.ts new file mode 100644 index 00000000..f5ff3418 --- /dev/null +++ b/packages/ctl/src/set_lazy_schema_compliation.ts @@ -0,0 +1,3 @@ +// This needs to be a separate file due to ECMAScript Modules evaluating code in +// imported modules before any code in the file that imports it. +(global as any).lazySchemaCompilation = true; diff --git a/packages/host/host.ts b/packages/host/host.ts index 91aeace5..a1f9a9b8 100755 --- a/packages/host/host.ts +++ b/packages/host/host.ts @@ -15,7 +15,7 @@ import fs from "node:fs/promises"; import path from "path"; import yargs from "yargs"; -import { version } from "./package.json"; +import packageConfig from "./package.json" with { type: "json" }; import winston from "winston"; import "winston-daily-rotate-file"; @@ -23,7 +23,7 @@ import "winston-daily-rotate-file"; import * as lib from "@clusterio/lib"; import { ConsoleTransport, levels, logger } from "@clusterio/lib"; -import Host from "./src/Host"; +import Host from "./src/Host.js"; let host: Host | undefined; @@ -69,7 +69,7 @@ export class HostConnector extends lib.WebSocketClientConnector { new lib.MessageRegisterHost( new lib.RegisterHostData( this.hostConfig.get("host.controller_token"), - version, + packageConfig.version, this.hostConfig.get("host.id"), plugins, ) @@ -145,7 +145,7 @@ async function startHost() { let command = args._[0]; if (command === "run") { - logger.info(`Starting Clusterio host ${version}`); + logger.info(`Starting Clusterio host ${packageConfig.version}`); if (args.recovery) { logger.warn("Host recovery mode enabled. Some features will be disabled."); } @@ -187,7 +187,7 @@ async function startHost() { } } - hostConfig.set("host.version", version); // Allows tracking last loaded version + hostConfig.set("host.version", packageConfig.version); // Allows tracking last loaded version if (command === "config") { await lib.handleConfigCommand(args, hostConfig, hostConfigLock); @@ -308,6 +308,6 @@ ${err.stack}` }); } -if (module === require.main) { +if (import.meta.main) { bootstrap(); } diff --git a/packages/host/index.ts b/packages/host/index.ts index f39ce536..75ef3560 100644 --- a/packages/host/index.ts +++ b/packages/host/index.ts @@ -1,10 +1,10 @@ -import { bootstrap } from "./host"; -export { default as BaseHostPlugin } from "./src/BaseHostPlugin"; -export { default as BaseInstancePlugin } from "./src/BaseInstancePlugin"; -export { default as Host, HostRouter } from "./src/Host"; -export { default as Instance } from "./src/Instance"; -export { default as InstanceConnection } from "./src/InstanceConnection"; +import { bootstrap } from "./host.js"; +export { default as BaseHostPlugin } from "./src/BaseHostPlugin.js"; +export { default as BaseInstancePlugin } from "./src/BaseInstancePlugin.js"; +export { default as Host, HostRouter } from "./src/Host.js"; +export { default as Instance } from "./src/Instance.js"; +export { default as InstanceConnection } from "./src/InstanceConnection.js"; -if (module === require.main) { +if (import.meta.main) { bootstrap(); } diff --git a/packages/host/package.json b/packages/host/package.json index e80c6082..542caa42 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -22,6 +22,7 @@ "access": "public" }, + "type": "module", "main": "dist/node/index.js", "scripts": { "prepare": "tsc --build", diff --git a/packages/host/src/BaseHostPlugin.ts b/packages/host/src/BaseHostPlugin.ts index 131da3db..d85f741a 100644 --- a/packages/host/src/BaseHostPlugin.ts +++ b/packages/host/src/BaseHostPlugin.ts @@ -1,7 +1,7 @@ import type { CollectorResult, Logger, ParsedFactorioOutput, PlayerEvent, PluginNodeEnvInfo, } from "@clusterio/lib"; -import type Host from "./Host"; +import type Host from "./Host.js"; /** * Base class for host plugins diff --git a/packages/host/src/BaseInstancePlugin.ts b/packages/host/src/BaseInstancePlugin.ts index 515af1ad..97d9efda 100644 --- a/packages/host/src/BaseInstancePlugin.ts +++ b/packages/host/src/BaseInstancePlugin.ts @@ -1,8 +1,8 @@ import type { CollectorResult, Logger, ParsedFactorioOutput, PlayerEvent, PluginNodeEnvInfo, } from "@clusterio/lib"; -import type Instance from "./Instance"; -import type Host from "./Host"; +import type Instance from "./Instance.js"; +import type Host from "./Host.js"; /** * Base class for instance plugins diff --git a/packages/host/src/Host.ts b/packages/host/src/Host.ts index 7cae0ce6..1af79a92 100644 --- a/packages/host/src/Host.ts +++ b/packages/host/src/Host.ts @@ -5,16 +5,17 @@ import pidusage from "pidusage"; import semver from "semver"; import setBlocking from "set-blocking"; import stream from "stream"; +import { fileURLToPath } from "node:url"; import util from "util"; // internal libraries import * as lib from "@clusterio/lib"; import { logger } from "@clusterio/lib"; -import type { HostConnector } from "../host"; -import Instance from "./Instance"; -import InstanceConnection from "./InstanceConnection"; -import BaseHostPlugin from "./BaseHostPlugin"; +import type { HostConnector } from "../host.js"; +import Instance from "./Instance.js"; +import InstanceConnection from "./InstanceConnection.js"; +import BaseHostPlugin from "./BaseHostPlugin.js"; const finished = util.promisify(stream.finished); @@ -859,7 +860,7 @@ export default class Host extends lib.Link { try { // First check the clusterio version const runningVersion = this.config.get("host.version"); - const packageJsonPath = require.resolve("@clusterio/host/package.json"); + const packageJsonPath = fileURLToPath(import.meta.resolve("@clusterio/host/package.json")); const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8")); if (runningVersion !== packageJson.version) { this.config.restartRequired = true; @@ -883,7 +884,7 @@ export default class Host extends lib.Link { } } } catch (err: any) { - logger.warn(`Failed to read package json for ${packageName}:\n${err.stack ?? err.message}`); + logger.warn(`Failed to read package.json for ${packageName}:\n${err.stack ?? err.message}`); } return false; @@ -892,7 +893,7 @@ export default class Host extends lib.Link { async checkRestartDowngrade() { try { const runningVersion = this.config.get("host.version"); - const packageJsonPath = require.resolve("@clusterio/host/package.json"); + const packageJsonPath = fileURLToPath(import.meta.resolve("@clusterio/host/package.json")); const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8")); const installedVersion = packageJson.version; diff --git a/packages/host/src/Instance.ts b/packages/host/src/Instance.ts index 3e45f04d..755c7364 100644 --- a/packages/host/src/Instance.ts +++ b/packages/host/src/Instance.ts @@ -9,11 +9,11 @@ const execAsync = util.promisify(exec); // internal libraries import * as lib from "@clusterio/lib"; -import { FactorioServer } from "./server"; -import { SaveModule, patch } from "./patch"; -import { exportData } from "./export"; -import type Host from "./Host"; -import BaseInstancePlugin from "./BaseInstancePlugin"; +import { FactorioServer } from "./server.js"; +import { SaveModule, patch } from "./patch.js"; +import { exportData } from "./export.js"; +import type Host from "./Host.js"; +import BaseInstancePlugin from "./BaseInstancePlugin.js"; const scriptCommands = [ "/cheat", "/editor", @@ -963,7 +963,7 @@ end`.replace(/\r?\n/g, " "); // Find stand alone modules to load // XXX for now only the included clusterio module is loaded - let modulesDirectory = path.join(__dirname, "..", "..", "..", "modules"); + let modulesDirectory = path.join(import.meta.dirname, "..", "..", "..", "modules"); for (let entry of await fs.readdir(modulesDirectory, { withFileTypes: true })) { if (entry.isDirectory()) { if (modules.has(entry.name)) { diff --git a/packages/host/src/InstanceConnection.ts b/packages/host/src/InstanceConnection.ts index e9f01053..5ae602be 100644 --- a/packages/host/src/InstanceConnection.ts +++ b/packages/host/src/InstanceConnection.ts @@ -1,6 +1,6 @@ import * as lib from "@clusterio/lib"; -import type Host from "./Host"; -import type Instance from "./Instance"; +import type Host from "./Host.js"; +import type Instance from "./Instance.js"; export default class InstanceConnection extends lib.Link { diff --git a/packages/host/src/export.ts b/packages/host/src/export.ts index 198ffb0a..e8986af2 100644 --- a/packages/host/src/export.ts +++ b/packages/host/src/export.ts @@ -5,9 +5,9 @@ import { Jimp, JimpMime, type JimpInstance } from "jimp"; import JSZip from "jszip"; import * as lib from "@clusterio/lib"; -import * as libBuildMod from "@clusterio/lib/dist/node/build_mod"; -import type { FactorioServer } from "./server"; -import { definesPrototypesFallback } from "./export_fallback"; +import * as libBuildMod from "@clusterio/lib/dist/node/build_mod.js"; +import type { FactorioServer } from "./server.js"; +import { definesPrototypesFallback } from "./export_fallback.js"; interface SimpleIconSpecification { icons?: never; @@ -56,7 +56,7 @@ async function generateExportMod(server: FactorioServer) { clean: false, build: true, pack: true, - sourceDir: path.join(__dirname, "..", "..", "..", "lua", "export"), + sourceDir: path.join(import.meta.dirname, "..", "..", "..", "lua", "export"), outputDir: server.writePath("mods"), bumpPatch: false, factorioVersion: server.version?.replace(/\.\d+$/, ""), diff --git a/packages/host/src/patch.ts b/packages/host/src/patch.ts index c94f5ce1..af89e6b0 100644 --- a/packages/host/src/patch.ts +++ b/packages/host/src/patch.ts @@ -9,7 +9,7 @@ import { Type, Static } from "@sinclair/typebox"; import * as lib from "@clusterio/lib"; -import BaseInstancePlugin from "./BaseInstancePlugin"; +import BaseInstancePlugin from "./BaseInstancePlugin.js"; /** diff --git a/packages/lib/browser.ts b/packages/lib/browser.ts index 1d7bc9c9..624196fb 100644 --- a/packages/lib/browser.ts +++ b/packages/lib/browser.ts @@ -17,7 +17,7 @@ export * from "./src/subscriptions"; export { default as ExponentialBackoff } from "./src/ExponentialBackoff"; export { default as RateLimiter } from "./src/RateLimiter"; export { default as ValueCache } from "./src/ValueCache"; -export { default as isDeepStrictEqual } from "./src/is_deep_strict_equal"; +export { default as isDeepStrictEqual } from "#is_deep_strict_equal"; import { checkSingletonImport } from "./src/check_singleton_import"; checkSingletonImport(import.meta.url); diff --git a/packages/lib/build_mod.js b/packages/lib/build_mod.js index d89c5dfb..d25ae4ba 100644 --- a/packages/lib/build_mod.js +++ b/packages/lib/build_mod.js @@ -1,11 +1,10 @@ /* eslint-disable no-console */ -"use strict"; -const fs = require("node:fs/promises"); -const JSZip = require("jszip"); -const path = require("path"); -const stream = require("stream"); -const util = require("util"); -const yargs = require("yargs"); +import fs from "node:fs/promises"; +import JSZip from "jszip"; +import path from "node:path"; +import stream from "node:stream"; +import util from "node:util"; +import yargs from "yargs"; const finished = util.promisify(stream.finished); @@ -79,7 +78,7 @@ async function buildMod(args, info, modName) { } } -async function build(args) { +export async function build(args) { let info = JSON.parse(await fs.readFile(path.join(args.sourceDir, "info.json"))); if (args.factorioVersion) { @@ -150,10 +149,6 @@ async function main() { await build(args); } -if (module === require.main) { +if (import.meta.main) { main().catch(err => { console.log(err); }); } - -module.exports = { - build, -}; diff --git a/packages/lib/index.ts b/packages/lib/index.ts index 48ed7462..116f289c 100644 --- a/packages/lib/index.ts +++ b/packages/lib/index.ts @@ -2,44 +2,44 @@ * Shared library for Clusterio * @module lib */ -export * from "./build_mod"; -export * from "./src/api"; -export * from "./src/command"; -export * from "./src/config"; -export * from "./src/data"; -export * from "./src/database"; -export * from "./src/errors"; -export * from "./src/export"; -export * from "./src/external"; -export * from "./src/factorio"; -export * from "./src/file_ops"; -export * from "./src/hash"; -export * from "./src/helpers"; -export * from "./src/ini"; -export * from "./src/link"; -export * from "./src/logging"; -export * from "./src/logging_utils"; -export * from "./src/lua_tools"; -export * from "./src/permissions"; -export * from "./src/plugin"; -export * from "./src/plugin_loader"; -export * from "./src/prometheus"; -export * from "./src/schema"; -export * from "./src/shared_commands"; -export * from "./src/stream"; -export * from "./src/system_collectors"; -export * from "./src/zip_ops"; -export * from "./src/subscriptions"; -export * from "./src/datastore"; -export * from "./src/load_plugin_list"; -export * from "./src/rce_ops"; -export * from "./src/LockFile"; +export * from "./build_mod.js"; +export * from "./src/api.js"; +export * from "./src/command.js"; +export * from "./src/config/index.js"; +export * from "./src/data/index.js"; +export * from "./src/database.js"; +export * from "./src/errors.js"; +export * from "./src/export.js"; +export * from "./src/external/index.js"; +export * from "./src/factorio/index.js"; +export * from "./src/file_ops.js"; +export * from "./src/hash.js"; +export * from "./src/helpers.js"; +export * from "./src/ini.js"; +export * from "./src/link/index.js"; +export * from "./src/logging.js"; +export * from "./src/logging_utils.js"; +export * from "./src/lua_tools.js"; +export * from "./src/permissions.js"; +export * from "./src/plugin.js"; +export * from "./src/plugin_loader.js"; +export * from "./src/prometheus.js"; +export * from "./src/schema.js"; +export * from "./src/shared_commands.js"; +export * from "./src/stream.js"; +export * from "./src/system_collectors.js"; +export * from "./src/zip_ops.js"; +export * from "./src/subscriptions.js"; +export * from "./src/datastore.js"; +export * from "./src/load_plugin_list.js"; +export * from "./src/rce_ops.js"; +export * from "./src/LockFile.js"; -export { default as ExponentialBackoff } from "./src/ExponentialBackoff"; -export { default as ModStore } from "./src/ModStore"; -export { default as RateLimiter } from "./src/RateLimiter"; -export { default as ValueCache } from "./src/ValueCache"; -export { default as isDeepStrictEqual } from "./src/is_deep_strict_equal"; +export { default as ExponentialBackoff } from "./src/ExponentialBackoff.js"; +export { default as ModStore } from "./src/ModStore.js"; +export { default as RateLimiter } from "./src/RateLimiter.js"; +export { default as ValueCache } from "./src/ValueCache.js"; +export { default as isDeepStrictEqual } from "#is_deep_strict_equal"; -import { checkSingletonImport } from "./src/check_singleton_import"; -checkSingletonImport(__filename); +import { checkSingletonImport } from "./src/check_singleton_import.js"; +checkSingletonImport(import.meta.filename); diff --git a/packages/lib/package.json b/packages/lib/package.json index 4da29bf8..8cc7bcb7 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -22,6 +22,17 @@ "access": "public" }, + "type": "module", + "imports": { + "#WebSocket": { + "node": "./src/link/WebSocket.node.ts", + "import": "./src/link/WebSocket.browser.ts" + }, + "#is_deep_strict_equal": { + "node": "./src/is_deep_strict_equal.node.ts", + "import": "./src/is_deep_strict_equal.browser.ts" + } + }, "browser": "browser.ts", "main": "dist/node/index.js", "scripts": { diff --git a/packages/lib/scripts/compile_validator.js b/packages/lib/scripts/compile_validator.js index 47bcee1e..7c1d932d 100644 --- a/packages/lib/scripts/compile_validator.js +++ b/packages/lib/scripts/compile_validator.js @@ -1,15 +1,14 @@ -"use strict"; -const fs = require("node:fs"); -const path = require("path"); -const Ajv = require("ajv"); -const standaloneCode = require("ajv/dist/standalone").default; +import fs from "node:fs"; +import path from "node:path"; +import Ajv from "ajv"; +import standaloneCode from "ajv/dist/standalone/index.js"; /* eslint-disable node/no-sync */ // Provide an empty file for message_core to load -fs.writeFileSync(path.join(__dirname, "../dist/node/src/data/message_validate.js"), ""); +fs.writeFileSync(path.join(import.meta.dirname, "../dist/node/src/data/message_validate.js"), "export default {};"); -const { Message } = require("../dist/node/src/data/messages_core"); +const { Message } = await import("../dist/node/src/data/messages_core.js"); const ajv = new Ajv({ allowUnionTypes: true, @@ -18,13 +17,14 @@ const ajv = new Ajv({ verbose: true, code: { source: true, + esm: true, }, }); const validate = ajv.compile(Message.jsonSchema); let moduleCode = standaloneCode(ajv, validate); function write(output) { fs.writeFileSync( - path.join(__dirname, output), + path.join(import.meta.dirname, output), `// generated by scripts/compile_validator.js\n// @ts-nocheck\n/* eslint-disable */\n${moduleCode}` ); } diff --git a/packages/lib/src/ModStore.ts b/packages/lib/src/ModStore.ts index 625ae253..999157f3 100644 --- a/packages/lib/src/ModStore.ts +++ b/packages/lib/src/ModStore.ts @@ -2,10 +2,10 @@ import fs from "node:fs/promises"; import path from "path"; import events from "events"; 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, MajorMinorVersion, ModVersionEquality, SourceVersion } from "./data"; +import { logger } from "./logging.js"; +import { downloadFile, safeOutputFile } from "./file_ops.js"; +import { ModPortalReleaseSchema, ModPortalDetailsSchema, ModNameVersionPair } from "./data/messages_mod.js"; +import { ModInfo, ModPack, MajorMinorVersion, ModVersionEquality, SourceVersion } from "./data/index.js"; export interface ModStoreEvents { /** A stored mod was created, updated or deleted */ diff --git a/packages/lib/src/check_singleton_import.ts b/packages/lib/src/check_singleton_import.ts index 6a734128..a238fbe8 100644 --- a/packages/lib/src/check_singleton_import.ts +++ b/packages/lib/src/check_singleton_import.ts @@ -1,4 +1,4 @@ -import { InstallationError } from "./errors"; +import { InstallationError } from "./errors.js"; declare global { // eslint-disable-next-line vars-on-top var _clusterioLibImportedFrom: string; diff --git a/packages/lib/src/command.ts b/packages/lib/src/command.ts index 2a55c22e..bf910262 100644 --- a/packages/lib/src/command.ts +++ b/packages/lib/src/command.ts @@ -6,9 +6,9 @@ * @author Hornwitser * @module lib/command */ -import * as libData from "./data"; -import * as libErrors from "./errors"; -import type { Link } from "./link"; +import * as libData from "./data/index.js"; +import * as libErrors from "./errors.js"; +import type { Link } from "./link/index.js"; import type { Argv } from "yargs"; export type CommandHandler = (args: any, control: any) => Promise; diff --git a/packages/lib/src/config/classes.ts b/packages/lib/src/config/classes.ts index d982f174..6ca54746 100644 --- a/packages/lib/src/config/classes.ts +++ b/packages/lib/src/config/classes.ts @@ -3,13 +3,13 @@ import { Type, Static } from "@sinclair/typebox"; import * as fs from "node:fs/promises"; import events from "events"; -import isDeepStrictEqual from "../is_deep_strict_equal"; -import { basicType } from "../helpers"; -import * as libSchema from "../schema"; -import { StringEnum } from "../data/composites"; -import { safeOutputFile } from "../file_ops"; -import * as validators from "./validators"; -import { RequestError } from "../errors"; +import isDeepStrictEqual from "#is_deep_strict_equal"; +import { basicType } from "../helpers.js"; +import * as libSchema from "../schema.js"; +import { StringEnum } from "../data/composites.js"; +import { safeOutputFile } from "../file_ops.js"; +import * as validators from "./validators.js"; +import { RequestError } from "../errors.js"; const ConfigLocation = StringEnum(["controller", "host", "control"]); export type ConfigLocation = Static; diff --git a/packages/lib/src/config/definitions.ts b/packages/lib/src/config/definitions.ts index 8510c280..0f109141 100644 --- a/packages/lib/src/config/definitions.ts +++ b/packages/lib/src/config/definitions.ts @@ -1,9 +1,9 @@ // Core definitions for the configuration system -import * as classes from "./classes"; -import * as validators from "./validators"; -import type { PluginNodeEnvInfo, PluginWebpackEnvInfo } from "../plugin"; +import * as classes from "./classes.js"; +import * as validators from "./validators.js"; +import type { PluginNodeEnvInfo, PluginWebpackEnvInfo } from "../plugin.js"; import { Static } from "@sinclair/typebox"; -import { isTargetVersion } from "../data/version"; +import { isTargetVersion } from "../data/version.js"; type configFromJSON = (...args: Parameters) => T; type configFromFile = (...args: Parameters) => Promise; diff --git a/packages/lib/src/config/index.ts b/packages/lib/src/config/index.ts index a111da14..0614402f 100644 --- a/packages/lib/src/config/index.ts +++ b/packages/lib/src/config/index.ts @@ -3,5 +3,5 @@ * @author Hornwitser * @module lib/config */ -export * from "./classes"; -export * from "./definitions"; +export * from "./classes.js"; +export * from "./definitions.js"; diff --git a/packages/lib/src/data/InstanceDetails.ts b/packages/lib/src/data/InstanceDetails.ts index 04c1a686..adf0b9d8 100644 --- a/packages/lib/src/data/InstanceDetails.ts +++ b/packages/lib/src/data/InstanceDetails.ts @@ -1,6 +1,6 @@ import { Static, Type } from "@sinclair/typebox"; -import { StringEnum } from "./composites"; -import { TargetVersion, TargetVersionSchema } from "./version"; +import { StringEnum } from "./composites.js"; +import { TargetVersion, TargetVersionSchema } from "./version.js"; export const InstanceStatus = StringEnum([ "unknown", diff --git a/packages/lib/src/data/ModInfo.ts b/packages/lib/src/data/ModInfo.ts index 3205e2aa..dc702d91 100644 --- a/packages/lib/src/data/ModInfo.ts +++ b/packages/lib/src/data/ModInfo.ts @@ -2,16 +2,16 @@ import fs from "node:fs/promises"; import JSZip from "jszip"; import { Type, Static } from "@sinclair/typebox"; -import * as libHash from "../hash"; -import * as libSchema from "../schema"; -import { findRoot } from "../zip_ops"; -import { ModRecord } from "./ModPack"; +import * as libHash from "../hash.js"; +import * as libSchema from "../schema.js"; +import { findRoot } from "../zip_ops.js"; +import { ModRecord } from "./ModPack.js"; import { MajorMinorVersion, MajorMinorVersionSchema, normaliseMajorMinorVersion, integerMajorMinorVersion, SourceVersion, SourceVersionSchema, isSourceVersion, normaliseSourceVersion, integerSourceVersion, ModVersionEquality, -} from "./version"; +} from "./version.js"; type ModDependencyType = "incompatible" | "optional" | "hidden" | "unordered" | "required" | "recommended"; diff --git a/packages/lib/src/data/ModPack.ts b/packages/lib/src/data/ModPack.ts index 33251845..29458422 100644 --- a/packages/lib/src/data/ModPack.ts +++ b/packages/lib/src/data/ModPack.ts @@ -1,19 +1,19 @@ import zlib from "zlib"; import { Type, Static } from "@sinclair/typebox"; -import * as libSchema from "../schema"; -import * as libLuaTools from "../lua_tools"; +import * as libSchema from "../schema.js"; +import * as libLuaTools from "../lua_tools.js"; -import ExportManifest from "./ExportManifest"; -import type { ExportSettings } from "../export"; -import type { Logger } from "../logging"; +import ExportManifest from "./ExportManifest.js"; +import type { ExportSettings } from "../export.js"; +import type { Logger } from "../logging.js"; -import ModInfo, { ModDependencyUnsatisfiedReason } from "./ModInfo"; +import ModInfo, { ModDependencyUnsatisfiedReason } from "./ModInfo.js"; import { PartialVersion, PartialVersionSchema, integerPartialVersion, SourceVersion, SourceVersionSchema, normaliseFullVersion, integerSourceVersion, -} from "./version"; +} from "./version.js"; export const ModSettingColor = Type.Object({ diff --git a/packages/lib/src/data/ModuleInfo.ts b/packages/lib/src/data/ModuleInfo.ts index 968a456e..8c43fc0b 100644 --- a/packages/lib/src/data/ModuleInfo.ts +++ b/packages/lib/src/data/ModuleInfo.ts @@ -1,5 +1,5 @@ import { Type, Static } from "@sinclair/typebox"; -import { compile } from "../schema"; +import { compile } from "../schema.js"; /** * Information about a module stored in its module.json file diff --git a/packages/lib/src/data/Role.ts b/packages/lib/src/data/Role.ts index 19fb17e7..954d5def 100644 --- a/packages/lib/src/data/Role.ts +++ b/packages/lib/src/data/Role.ts @@ -1,5 +1,5 @@ import { Static, Type } from "@sinclair/typebox"; -import { permissions as globalPermissions } from "../permissions"; +import { permissions as globalPermissions } from "../permissions.js"; interface RoleCollection { get(id: number): Role; diff --git a/packages/lib/src/data/UserDetails.ts b/packages/lib/src/data/UserDetails.ts index 5a99dfed..28a1646a 100644 --- a/packages/lib/src/data/UserDetails.ts +++ b/packages/lib/src/data/UserDetails.ts @@ -1,6 +1,6 @@ import { Static, Type } from "@sinclair/typebox"; -import PlayerStats from "./PlayerStats"; +import PlayerStats from "./PlayerStats.js"; /** Represents a user in the cluster */ export default class UserDetails { diff --git a/packages/lib/src/data/index.ts b/packages/lib/src/data/index.ts index a8ca8a96..ff473748 100644 --- a/packages/lib/src/data/index.ts +++ b/packages/lib/src/data/index.ts @@ -3,26 +3,26 @@ * @module lib/data * @author Hornwitser */ -export { default as ExportManifest } from "./ExportManifest"; -export { default as ModInfo, ModDependency } from "./ModInfo"; -export { ModSettingColor, getInstalledModUpdates, applyModRecordAdvisories } from "./ModPack"; -export type { ModSetting, ModRecord, ModRecordAdvisory } from "./ModPack"; -export { default as ModPack } from "./ModPack"; -export { default as ModuleInfo } from "./ModuleInfo"; -export { default as Permission } from "./Permission"; -export { default as PlayerStats } from "./PlayerStats"; -export { default as Role } from "./Role"; -export { default as InstanceDetails, InstanceStatus } from "./InstanceDetails"; -export { default as HostDetails } from "./HostDetails"; -export { default as UserDetails } from "./UserDetails"; -export type { IUser } from "./UserDetails"; -export * from "./composites"; -export * from "./messages_core"; -export * from "./messages_controller"; -export * from "./messages_host"; -export * from "./messages_instance"; -export * from "./messages_mod"; -export * from "./messages_user"; -export * from "./messages_role"; -export * from "./messages_plugin"; -export * from "./version"; +export { default as ExportManifest } from "./ExportManifest.js"; +export { default as ModInfo, ModDependency } from "./ModInfo.js"; +export { ModSettingColor, getInstalledModUpdates, applyModRecordAdvisories } from "./ModPack.js"; +export type { ModSetting, ModRecord, ModRecordAdvisory } from "./ModPack.js"; +export { default as ModPack } from "./ModPack.js"; +export { default as ModuleInfo } from "./ModuleInfo.js"; +export { default as Permission } from "./Permission.js"; +export { default as PlayerStats } from "./PlayerStats.js"; +export { default as Role } from "./Role.js"; +export { default as InstanceDetails, InstanceStatus } from "./InstanceDetails.js"; +export { default as HostDetails } from "./HostDetails.js"; +export { default as UserDetails } from "./UserDetails.js"; +export type { IUser } from "./UserDetails.js"; +export * from "./composites.js"; +export * from "./messages_core.js"; +export * from "./messages_controller.js"; +export * from "./messages_host.js"; +export * from "./messages_instance.js"; +export * from "./messages_mod.js"; +export * from "./messages_user.js"; +export * from "./messages_role.js"; +export * from "./messages_plugin.js"; +export * from "./version.js"; diff --git a/packages/lib/src/data/message_validate.d.ts b/packages/lib/src/data/message_validate.d.ts index d9ec5838..bbb3d8b3 100644 --- a/packages/lib/src/data/message_validate.d.ts +++ b/packages/lib/src/data/message_validate.d.ts @@ -1,4 +1,4 @@ import type { ValidateFunction } from "ajv"; -import type { Message } from "./messages_core"; +import type { Message } from "./messages_core.js"; const message_validate: ValidateFunction; export default message_validate; diff --git a/packages/lib/src/data/messages_controller.ts b/packages/lib/src/data/messages_controller.ts index 9075cfb9..25ca7789 100644 --- a/packages/lib/src/data/messages_controller.ts +++ b/packages/lib/src/data/messages_controller.ts @@ -1,8 +1,8 @@ import { Type, Static } from "@sinclair/typebox"; -import { JsonString, StringEnum, StringKey, plainJson } from "./composites"; -import { levels } from "../logging"; -import { ControllerConfig, HostConfig } from "../config"; -import { ExternalFactorioVersionSchema, LatestReleasesSchema } from "../external"; +import { JsonString, StringEnum, StringKey, plainJson } from "./composites.js"; +import { levels } from "../logging.js"; +import { ControllerConfig, HostConfig } from "../config/index.js"; +import { ExternalFactorioVersionSchema, LatestReleasesSchema } from "../external/index.js"; export class ControllerStopRequest { declare ["constructor"]: typeof ControllerStopRequest; diff --git a/packages/lib/src/data/messages_core.ts b/packages/lib/src/data/messages_core.ts index c4145a47..ff391b21 100644 --- a/packages/lib/src/data/messages_core.ts +++ b/packages/lib/src/data/messages_core.ts @@ -1,8 +1,8 @@ // Note: Run compile_validator script after making changes to this file. import { TSchema, Type, Static } from "@sinclair/typebox"; -import messageValidate from "./message_validate"; // generated file -import { StringEnum } from "./composites"; +import messageValidate from "./message_validate.js"; // generated file +import { StringEnum } from "./composites.js"; export type AddressType = "controller" | "host" | "instance" | "control" | "broadcast"; export type AddressShorthand = diff --git a/packages/lib/src/data/messages_host.ts b/packages/lib/src/data/messages_host.ts index e707d856..aa4edbc9 100644 --- a/packages/lib/src/data/messages_host.ts +++ b/packages/lib/src/data/messages_host.ts @@ -1,8 +1,8 @@ import { Type, Static } from "@sinclair/typebox"; -import { jsonArray, plainJson, StringEnum } from "./composites"; -import { CollectorResultSerialized } from "../prometheus"; -import { HostConfig } from "../config/definitions"; -import HostDetails from "./HostDetails"; +import { jsonArray, plainJson, StringEnum } from "./composites.js"; +import { CollectorResultSerialized } from "../prometheus.js"; +import { HostConfig } from "../config/definitions.js"; +import HostDetails from "./HostDetails.js"; export class HostStopRequest { declare ["constructor"]: typeof HostStopRequest; diff --git a/packages/lib/src/data/messages_instance.ts b/packages/lib/src/data/messages_instance.ts index 51bcd0db..d107632c 100644 --- a/packages/lib/src/data/messages_instance.ts +++ b/packages/lib/src/data/messages_instance.ts @@ -1,12 +1,12 @@ import { Type, Static } from "@sinclair/typebox"; -import PlayerStats from "./PlayerStats"; -import { JsonString, StringEnum, jsonArray, plainJson } from "./composites"; -import { InstanceConfig } from "../config"; -import type { IUser } from "./UserDetails"; -import type { MessageRequest } from "./messages_core"; -import { CollectorResultSerialized } from "../prometheus"; -import { TargetVersion, TargetVersionSchema, PartialVersion, PartialVersionSchema } from "./version"; -import InstanceDetails, { InstanceStatus } from "./InstanceDetails"; +import PlayerStats from "./PlayerStats.js"; +import { JsonString, StringEnum, jsonArray, plainJson } from "./composites.js"; +import { InstanceConfig } from "../config/index.js"; +import type { IUser } from "./UserDetails.js"; +import type { MessageRequest } from "./messages_core.js"; +import { CollectorResultSerialized } from "../prometheus.js"; +import { TargetVersion, TargetVersionSchema, PartialVersion, PartialVersionSchema } from "./version.js"; +import InstanceDetails, { InstanceStatus } from "./InstanceDetails.js"; export class InstanceDetailsGetRequest { declare ["constructor"]: typeof InstanceDetailsGetRequest; diff --git a/packages/lib/src/data/messages_mod.ts b/packages/lib/src/data/messages_mod.ts index 4d5a09e3..5d26fc78 100644 --- a/packages/lib/src/data/messages_mod.ts +++ b/packages/lib/src/data/messages_mod.ts @@ -1,13 +1,13 @@ import { Type, Static } from "@sinclair/typebox"; -import ModInfo, { ModDependency } from "./ModInfo"; -import ModPack from "./ModPack"; -import { JsonString, jsonArray } from "./composites"; +import ModInfo, { ModDependency } from "./ModInfo.js"; +import ModPack from "./ModPack.js"; +import { JsonString, jsonArray } from "./composites.js"; import { MajorMinorVersion, MajorMinorVersionSchema, normaliseMajorMinorVersion, SourceVersion, SourceVersionSchema, ModVersionEquality, -} from "./version"; +} from "./version.js"; export class ModPackGetRequest { diff --git a/packages/lib/src/data/messages_plugin.ts b/packages/lib/src/data/messages_plugin.ts index edbe7664..2c2a114c 100644 --- a/packages/lib/src/data/messages_plugin.ts +++ b/packages/lib/src/data/messages_plugin.ts @@ -1,6 +1,6 @@ import { Type, Static } from "@sinclair/typebox"; -import { jsonArray } from "./composites"; -import { PluginNodeEnvInfo } from "../plugin"; +import { jsonArray } from "./composites.js"; +import { PluginNodeEnvInfo } from "../plugin.js"; /* This is similar to other plugin definitions but with lots removed */ export class PluginDetails { diff --git a/packages/lib/src/data/messages_role.ts b/packages/lib/src/data/messages_role.ts index 423c1d65..1a3986cb 100644 --- a/packages/lib/src/data/messages_role.ts +++ b/packages/lib/src/data/messages_role.ts @@ -1,7 +1,7 @@ import { Type, Static } from "@sinclair/typebox"; -import { JsonNumber, jsonArray } from "./composites"; -import Permission from "./Permission"; -import Role from "./Role"; +import { JsonNumber, jsonArray } from "./composites.js"; +import Permission from "./Permission.js"; +import Role from "./Role.js"; export class PermissionListRequest { declare ["constructor"]: typeof PermissionListRequest; diff --git a/packages/lib/src/data/messages_user.ts b/packages/lib/src/data/messages_user.ts index 2c73d8be..2de27c19 100644 --- a/packages/lib/src/data/messages_user.ts +++ b/packages/lib/src/data/messages_user.ts @@ -1,7 +1,7 @@ import { Type, Static } from "@sinclair/typebox"; -import UserDetails, { IUser } from "./UserDetails"; -import { StringEnum, jsonArray, plainJson } from "./composites"; -import { MessageRequest } from "./messages_core"; +import UserDetails, { IUser } from "./UserDetails.js"; +import { StringEnum, jsonArray, plainJson } from "./composites.js"; +import { MessageRequest } from "./messages_core.js"; export class UserGetRequest { declare ["constructor"]: typeof UserGetRequest; diff --git a/packages/lib/src/database.ts b/packages/lib/src/database.ts index 81ea71ac..1e94d298 100644 --- a/packages/lib/src/database.ts +++ b/packages/lib/src/database.ts @@ -6,8 +6,8 @@ */ import fs from "node:fs/promises"; -import * as libFileOps from "./file_ops"; -import { basicType } from "./helpers"; +import * as libFileOps from "./file_ops.js"; +import { basicType } from "./helpers.js"; export type ItemCountWithQuality = { [quality: string]: number }; diff --git a/packages/lib/src/datastore.ts b/packages/lib/src/datastore.ts index ed5113d9..23c294e0 100644 --- a/packages/lib/src/datastore.ts +++ b/packages/lib/src/datastore.ts @@ -2,10 +2,10 @@ import path from "path"; import fs from "node:fs/promises"; import { EventEmitter } from "events"; -import { safeOutputFile } from "./file_ops"; -import { SubscribableValue } from "./subscriptions"; -import { logger } from "./logging"; -import { ControllerConfig } from "./config"; +import { safeOutputFile } from "./file_ops.js"; +import { SubscribableValue } from "./subscriptions.js"; +import { logger } from "./logging.js"; +import { ControllerConfig } from "./config/index.js"; type DatastoreKey = string | number; type DatastoreValue = string | number | boolean | object; diff --git a/packages/lib/src/export.ts b/packages/lib/src/export.ts index 10f06089..2ae9eb3f 100644 --- a/packages/lib/src/export.ts +++ b/packages/lib/src/export.ts @@ -1,4 +1,4 @@ -import { FactorioColor } from "./lua_tools"; +import { FactorioColor } from "./lua_tools.js"; /** * A localised string. diff --git a/packages/lib/src/external/FactorioVersions.ts b/packages/lib/src/external/FactorioVersions.ts index 86360b39..ae1f2e39 100644 --- a/packages/lib/src/external/FactorioVersions.ts +++ b/packages/lib/src/external/FactorioVersions.ts @@ -1,5 +1,5 @@ import { Static, Type } from "@sinclair/typebox"; -import { FullVersionSchema, integerFullVersion, isFullVersion } from "../data/version"; +import { FullVersionSchema, integerFullVersion, isFullVersion } from "../data/version.js"; const ARCHIVE_URL = "https://factorio.com/download/archive/"; diff --git a/packages/lib/src/external/LatestReleases.ts b/packages/lib/src/external/LatestReleases.ts index 91a0f322..885565ae 100644 --- a/packages/lib/src/external/LatestReleases.ts +++ b/packages/lib/src/external/LatestReleases.ts @@ -1,5 +1,5 @@ import { Static, Type } from "@sinclair/typebox"; -import { FullVersion, FullVersionSchema } from "../data/version"; +import { FullVersion, FullVersionSchema } from "../data/version.js"; const LATEST_RELEASES_URL = "https://factorio.com/api/latest-releases"; diff --git a/packages/lib/src/external/index.ts b/packages/lib/src/external/index.ts index 0bd258c4..0bf27ace 100644 --- a/packages/lib/src/external/index.ts +++ b/packages/lib/src/external/index.ts @@ -2,5 +2,5 @@ * Library for interfacing with external apis * @module lib/external */ -export * from "./FactorioVersions"; -export * from "./LatestReleases"; +export * from "./FactorioVersions.js"; +export * from "./LatestReleases.js"; diff --git a/packages/lib/src/factorio/index.ts b/packages/lib/src/factorio/index.ts index 72335610..dbf0e1bf 100644 --- a/packages/lib/src/factorio/index.ts +++ b/packages/lib/src/factorio/index.ts @@ -3,4 +3,4 @@ * @module lib/factorio * @author Hornwitser */ -export * from "./exchange_string"; +export * from "./exchange_string.js"; diff --git a/packages/lib/src/file_ops.ts b/packages/lib/src/file_ops.ts index 55d2bda7..b8899406 100644 --- a/packages/lib/src/file_ops.ts +++ b/packages/lib/src/file_ops.ts @@ -7,7 +7,7 @@ import fs from "node:fs/promises"; import path from "path"; import stream from "stream"; -import { redactUrl } from "./helpers"; +import { redactUrl } from "./helpers.js"; /** diff --git a/packages/lib/src/helpers.ts b/packages/lib/src/helpers.ts index b14d8e2a..dbbf2f26 100644 --- a/packages/lib/src/helpers.ts +++ b/packages/lib/src/helpers.ts @@ -3,7 +3,7 @@ * @module lib/helpers */ -import type { Logger } from "./logging"; +import type { Logger } from "./logging.js"; import setBlocking from "set-blocking"; /** diff --git a/packages/lib/src/is_deep_strict_equal.browser.ts b/packages/lib/src/is_deep_strict_equal.browser.ts new file mode 100644 index 00000000..9fdd65d2 --- /dev/null +++ b/packages/lib/src/is_deep_strict_equal.browser.ts @@ -0,0 +1 @@ +export { default } from "fast-deep-equal/es6/index.js"; diff --git a/packages/lib/src/is_deep_strict_equal.d.ts b/packages/lib/src/is_deep_strict_equal.d.ts deleted file mode 100644 index 3ec0532c..00000000 --- a/packages/lib/src/is_deep_strict_equal.d.ts +++ /dev/null @@ -1 +0,0 @@ -export default function isDeepStrictEqual(a: any, b: any): boolean; diff --git a/packages/lib/src/is_deep_strict_equal.js b/packages/lib/src/is_deep_strict_equal.js deleted file mode 100644 index 0f695078..00000000 --- a/packages/lib/src/is_deep_strict_equal.js +++ /dev/null @@ -1,9 +0,0 @@ - -"use strict"; - -if (process.env.APP_ENV === "browser") { - - module.exports = require("fast-deep-equal/es6"); -} else { - module.exports = require("util").isDeepStrictEqual; -} diff --git a/packages/lib/src/is_deep_strict_equal.node.ts b/packages/lib/src/is_deep_strict_equal.node.ts new file mode 100644 index 00000000..4714580c --- /dev/null +++ b/packages/lib/src/is_deep_strict_equal.node.ts @@ -0,0 +1 @@ +export { isDeepStrictEqual as default } from "util"; diff --git a/packages/lib/src/link/WebSocket.browser.ts b/packages/lib/src/link/WebSocket.browser.ts new file mode 100644 index 00000000..a1444375 --- /dev/null +++ b/packages/lib/src/link/WebSocket.browser.ts @@ -0,0 +1,2 @@ +const NativeWebSocket = WebSocket; +export { NativeWebSocket as WebSocket }; diff --git a/packages/lib/src/link/WebSocket.d.ts b/packages/lib/src/link/WebSocket.d.ts deleted file mode 100644 index c44f2672..00000000 --- a/packages/lib/src/link/WebSocket.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -import ws = require("ws"); -export = ws; diff --git a/packages/lib/src/link/WebSocket.js b/packages/lib/src/link/WebSocket.js deleted file mode 100644 index 06879e25..00000000 --- a/packages/lib/src/link/WebSocket.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict"; - -module.exports = process.env.APP_ENV === "browser" ? window.WebSocket : require("ws"); diff --git a/packages/lib/src/link/WebSocket.node.ts b/packages/lib/src/link/WebSocket.node.ts new file mode 100644 index 00000000..64683e43 --- /dev/null +++ b/packages/lib/src/link/WebSocket.node.ts @@ -0,0 +1 @@ +export { WebSocket } from "ws"; diff --git a/packages/lib/src/link/connectors.ts b/packages/lib/src/link/connectors.ts index ebed9978..8ce84f8f 100644 --- a/packages/lib/src/link/connectors.ts +++ b/packages/lib/src/link/connectors.ts @@ -2,12 +2,12 @@ import { strict as assert } from "assert"; import events from "events"; -import WebSocket from "./WebSocket"; -import * as libData from "../data"; -import * as libErrors from "../errors"; -import { logger } from "../logging"; -import ExponentialBackoff from "../ExponentialBackoff"; -import type { Request, Event } from "./link"; +import { WebSocket } from "#WebSocket"; +import * as libData from "../data/index.js"; +import * as libErrors from "../errors.js"; +import { logger } from "../logging.js"; +import ExponentialBackoff from "../ExponentialBackoff.js"; +import type { Request, Event } from "./link.js"; /** * Numbered codes describing why a connection was closed @@ -151,7 +151,7 @@ type WebSocketBaseConnectorEvents = { "drop": [], }; -export type WebSocketClusterio = WebSocket.WebSocket & { +export type WebSocketClusterio = WebSocket & { clusterio_ignore_dump?: boolean; }; @@ -275,7 +275,9 @@ export abstract class WebSocketBaseConnector< if (process.env.APP_ENV === "browser") { this._socket!.close(ConnectionClosed.Timeout, "Heartbeat timeout"); } else { - this._socket!.terminate(); + // This is only available in node, but the typechecking doesn't + // detect that this code is unreachable in the browser build. + (this._socket! as any).terminate(); } } else { @@ -635,7 +637,7 @@ export abstract class WebSocketClientConnector extends WebSocketBaseConnector { + this._socket!.onerror = ((event: { error?: any, message?: string, }) => { // It's assumed that close is always called by ws let code = !event.error ? "" : `, code: ${event.error.code}`; let message = `Connector | Socket error: ${event.message || "unknown error"}${code}`; @@ -657,7 +659,8 @@ export abstract class WebSocketClientConnector extends WebSocketBaseConnector { logger.verbose("Connector | Open"); diff --git a/packages/lib/src/link/index.ts b/packages/lib/src/link/index.ts index d48297e3..e45a6550 100644 --- a/packages/lib/src/link/index.ts +++ b/packages/lib/src/link/index.ts @@ -3,5 +3,5 @@ * @module lib/link * @author Hornwitser */ -export * from "./link"; -export * from "./connectors"; +export * from "./link.js"; +export * from "./connectors.js"; diff --git a/packages/lib/src/link/link.ts b/packages/lib/src/link/link.ts index c4c8fa9b..80973177 100644 --- a/packages/lib/src/link/link.ts +++ b/packages/lib/src/link/link.ts @@ -1,15 +1,15 @@ // Implementation of Link class -import * as libData from "../data"; -import * as libErrors from "../errors"; -import { logger } from "../logging"; -import * as libSchema from "../schema"; -import { dataClasses } from "./messages"; -import { BaseConnector, WebSocketBaseConnector } from "./connectors"; +import * as libData from "../data/index.js"; +import * as libErrors from "../errors.js"; +import { logger } from "../logging.js"; +import * as libSchema from "../schema.js"; +import { dataClasses } from "./messages.js"; +import { BaseConnector, WebSocketBaseConnector } from "./connectors.js"; import { strict as assert } from "assert"; -import type { PluginNodeEnvInfo, PluginWebpackEnvInfo } from "../plugin"; +import type { PluginNodeEnvInfo, PluginWebpackEnvInfo } from "../plugin.js"; import type { AddressType, JSONDeserialisable, MessageRoutable, MessageRequest, MessageEvent, IUser, -} from "../data"; +} from "../data/index.js"; export interface Request { constructor: Partial>> & { diff --git a/packages/lib/src/link/messages.ts b/packages/lib/src/link/messages.ts index 1db48338..dbdac526 100644 --- a/packages/lib/src/link/messages.ts +++ b/packages/lib/src/link/messages.ts @@ -1,13 +1,13 @@ -import * as core from "../data/messages_core"; -import * as controller from "../data/messages_controller"; -import * as host from "../data/messages_host"; -import * as instance from "../data/messages_instance"; -import * as mod from "../data/messages_mod"; -import * as user from "../data/messages_user"; -import * as role from "../data/messages_role"; -import * as plugin from "../data/messages_plugin"; -import * as subscriptions from "../subscriptions"; -import type { RequestClass, EventClass } from "./link"; +import * as core from "../data/messages_core.js"; +import * as controller from "../data/messages_controller.js"; +import * as host from "../data/messages_host.js"; +import * as instance from "../data/messages_instance.js"; +import * as mod from "../data/messages_mod.js"; +import * as user from "../data/messages_user.js"; +import * as role from "../data/messages_role.js"; +import * as plugin from "../data/messages_plugin.js"; +import * as subscriptions from "../subscriptions.js"; +import type { RequestClass, EventClass } from "./link.js"; /** * Event and Request payloads registered with the Link class. Used to decode diff --git a/packages/lib/src/load_plugin_list.ts b/packages/lib/src/load_plugin_list.ts index 8d4d1689..d91977e2 100644 --- a/packages/lib/src/load_plugin_list.ts +++ b/packages/lib/src/load_plugin_list.ts @@ -1,8 +1,8 @@ import fs from "node:fs/promises"; import { createRequire } from "node:module"; import path from "path"; -import { logger } from "./logging"; -import * as libFileOps from "./file_ops"; +import { logger } from "./logging.js"; +import * as libFileOps from "./file_ops.js"; import vm from "vm"; /** @@ -67,6 +67,7 @@ async function findLocalPlugins(pluginList: Map): Promise 0; } +const require = createRequire(import.meta.url); function getPluginName(requireSpec: string) { const context = vm.createContext({ require: require, pluginInfo: null }); const code = `pluginInfo = require(${JSON.stringify(requireSpec)}).plugin;`; diff --git a/packages/lib/src/logging_utils.ts b/packages/lib/src/logging_utils.ts index eebb1c2e..2a02f57d 100644 --- a/packages/lib/src/logging_utils.ts +++ b/packages/lib/src/logging_utils.ts @@ -14,12 +14,12 @@ import Transport from "winston-transport"; import { LEVEL, MESSAGE } from "triple-beam"; import chalk from "chalk"; -import * as libData from "./data"; -import * as libErrors from "./errors"; -import * as libFileOps from "./file_ops"; -import { type LogFilter, levels, logFilter, logger } from "./logging"; -import * as libStream from "./stream"; -import type { Link } from "./link"; +import * as libData from "./data/index.js"; +import * as libErrors from "./errors.js"; +import * as libFileOps from "./file_ops.js"; +import { type LogFilter, levels, logFilter, logger } from "./logging.js"; +import * as libStream from "./stream.js"; +import type { Link } from "./link/index.js"; const finished = util.promisify(stream.finished); diff --git a/packages/lib/src/permissions.ts b/packages/lib/src/permissions.ts index ce1bb44c..b7ef91a9 100644 --- a/packages/lib/src/permissions.ts +++ b/packages/lib/src/permissions.ts @@ -6,7 +6,7 @@ * @author Hornwitser * @module lib/users */ -import { Permission } from "./data"; +import { Permission } from "./data/index.js"; export const permissions = new Map(); diff --git a/packages/lib/src/plugin.ts b/packages/lib/src/plugin.ts index 456cde2a..b5060561 100644 --- a/packages/lib/src/plugin.ts +++ b/packages/lib/src/plugin.ts @@ -2,10 +2,10 @@ * Plugin interfaces and utilities. * @module lib/plugin */ -import * as libHelpers from "./helpers"; -import type { Logger } from "./logging"; -import type { FieldDefinition } from "./config"; -import type { PlayerStats } from "./data"; +import * as libHelpers from "./helpers.js"; +import type { Logger } from "./logging.js"; +import type { FieldDefinition } from "./config/index.js"; +import type { PlayerStats } from "./data/index.js"; export const PluginFeatureFlags = [ diff --git a/packages/lib/src/plugin_loader.ts b/packages/lib/src/plugin_loader.ts index 31ec7a03..7578e0f7 100644 --- a/packages/lib/src/plugin_loader.ts +++ b/packages/lib/src/plugin_loader.ts @@ -5,9 +5,10 @@ */ import path from "path"; import fs from "node:fs/promises"; -import * as libErrors from "./errors"; -import * as libPlugin from "./plugin"; -import { logger } from "./logging"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import * as libErrors from "./errors.js"; +import * as libPlugin from "./plugin.js"; +import { logger } from "./logging.js"; /** @@ -32,7 +33,9 @@ export async function loadPluginInfos(pluginList: Map) { try { const absolute = path.isAbsolute(pluginPath); const packageImport = path.posix.join(pluginPath, "package.json"); - packagePath = require.resolve(packageImport); + packagePath = fileURLToPath(import.meta.resolve( + absolute ? pathToFileURL(packageImport).href : packageImport + )); await fs.access(packagePath, fs.constants.F_OK); } catch (err) { let errMsg = `Plugin path ${pluginPath} does not exist`; @@ -46,8 +49,10 @@ export async function loadPluginInfos(pluginList: Map) { } try { - pluginInfo = require(pluginPath).plugin; - pluginPackage = require(packagePath); + pluginPackage = (await import(pathToFileURL(packagePath).href, { with: { type: "json" }})).default; + pluginInfo = (await import( + pathToFileURL(path.posix.join(pluginPath, pluginPackage.main ?? "index.js")).href + )).plugin; } catch (err: any) { if (err.code === "InstallationError") { throw err; @@ -89,7 +94,7 @@ export async function loadPluginClass className: string, pluginClass: Class, ): Promise { - let entrypoint = require(requirePath); + let entrypoint = await import(pathToFileURL(requirePath).href); if (!entrypoint[className]) { throw new libErrors.PluginError(pluginName, new Error(`Expected ${requirePath} to export a class named ${className}`) diff --git a/packages/lib/src/prometheus.ts b/packages/lib/src/prometheus.ts index 84999093..89082788 100644 --- a/packages/lib/src/prometheus.ts +++ b/packages/lib/src/prometheus.ts @@ -12,7 +12,7 @@ * the listed interfaces for more information. * * @example - * const { Counter, exposition } = require("@clusterio/lib"); + * import { Counter, exposition } from "@clusterio/lib"; * * // Collectors are by default registered to the default collector regitry. * const totalRequests = new Counter( @@ -27,7 +27,8 @@ * * // Code handling the /metrics HTTP request, here shown for express * // but any http framework may be used - * const app = require("express")(); + * import express from "express"; + * const app = express(); * async function getMetrics(req, res) { * // By default exposition uses the default collector registry * let text = await exposition(); @@ -39,7 +40,7 @@ * @module lib/prometheus */ import { Type, Static } from "@sinclair/typebox"; -import { StringEnum } from "./data/composites"; +import { StringEnum } from "./data/composites.js"; /** * Result from collecting a {@link Collector} @@ -1669,7 +1670,8 @@ async function* expositionLines( * express.js. * * @example - * const app = require("express")(); + * import express from "express"; + * const app = express(); * async function getMetrics(req, res) { * // By default exposition uses the default collector registry * let text = await exposition(); diff --git a/packages/lib/src/rce_ops.ts b/packages/lib/src/rce_ops.ts index b5ad5be5..92f32adc 100644 --- a/packages/lib/src/rce_ops.ts +++ b/packages/lib/src/rce_ops.ts @@ -1,15 +1,15 @@ import util from "util"; import path from "path"; import { exec } from "child_process"; -import { logger } from "./logging"; -import { RequestError } from "./errors"; -import { PluginNodeEnvInfo } from "./plugin"; +import { logger } from "./logging.js"; +import { RequestError } from "./errors.js"; +import { PluginNodeEnvInfo } from "./plugin.js"; const execAsync = util.promisify(exec); function isDev() { // dev: /packages/lib/dist/src/rce_ops.js // prod: /node_modules/@clusterio/lib/dist/src/rce_ops.js - return __dirname.split(path.sep).at(-5) === "packages"; // opposed to "@clusterio" + return import.meta.dirname.split(path.sep).at(-5) === "packages"; // opposed to "@clusterio" } async function logExec(cmd: string) { diff --git a/packages/lib/src/schema.ts b/packages/lib/src/schema.ts index c783a3c9..4224086b 100644 --- a/packages/lib/src/schema.ts +++ b/packages/lib/src/schema.ts @@ -2,7 +2,7 @@ * JSON schemas used for validating link messages * @module lib/schema */ -import Ajv, { JSONSchemaType, ValidateFunction } from "ajv"; +import { Ajv, JSONSchemaType, ValidateFunction } from "ajv"; const ajv = new Ajv({ // These are duplicated in scripts/compile_validator allowUnionTypes: true, diff --git a/packages/lib/src/shared_commands.ts b/packages/lib/src/shared_commands.ts index d9f6eb09..654f0d90 100644 --- a/packages/lib/src/shared_commands.ts +++ b/packages/lib/src/shared_commands.ts @@ -4,11 +4,12 @@ */ import path from "path"; -import * as libConfig from "./config"; -import * as libFileOps from "./file_ops"; -import { logger } from "./logging"; -import * as libHelpers from "./helpers"; -import { LockFile } from "./LockFile"; +import * as libConfig from "./config/index.js"; +import * as libFileOps from "./file_ops.js"; +import { logger } from "./logging.js"; +import * as libHelpers from "./helpers.js"; +import { LockFile } from "./LockFile.js"; +import { pathToFileURL } from "url"; function print(...content: any[]) { @@ -59,8 +60,13 @@ export async function handlePluginCommand( let pluginInfo: { name: string }; try { - - pluginInfo = require(pluginPath).plugin; + const pluginPackage = (await import( + pathToFileURL(path.posix.join(pluginPath, "package.json")).href, + { with: { type: "json" }}, + )).default; + pluginInfo = (await import( + pathToFileURL(path.posix.join(pluginPath, pluginPackage.main ?? "index.js")).href + )).plugin; } catch (err: any) { logger.error(`Unable to import plugin info from ${args.path}:\n${err.stack}`); process.exitCode = 1; diff --git a/packages/lib/src/subscriptions.ts b/packages/lib/src/subscriptions.ts index 1b3311b6..4324f53e 100644 --- a/packages/lib/src/subscriptions.ts +++ b/packages/lib/src/subscriptions.ts @@ -1,9 +1,9 @@ import { Type, Static } from "@sinclair/typebox"; -import { Link, Event, EventClass, RequestHandler, WebSocketBaseConnector } from "./link"; -import { Address, MessageRequest, IUser, JsonBoolean, StringEnum, AccountDetails } from "./data"; -import isDeepStrictEqual from "./is_deep_strict_equal"; -import { RequestError } from "./errors"; -import { logger } from "./logging"; +import { Link, Event, EventClass, RequestHandler, WebSocketBaseConnector } from "./link/index.js"; +import { Address, MessageRequest, IUser, JsonBoolean, StringEnum } from "./data/index.js"; +import isDeepStrictEqual from "#is_deep_strict_equal"; +import { RequestError } from "./errors.js"; +import { logger } from "./logging.js"; export type SubscriptionRequestHandler = RequestHandler | null>; export type EventSubscriberCallback = (event: T | null, synced: boolean) => void diff --git a/packages/lib/src/system_collectors.ts b/packages/lib/src/system_collectors.ts index adadc0ad..6132dfe2 100644 --- a/packages/lib/src/system_collectors.ts +++ b/packages/lib/src/system_collectors.ts @@ -1,8 +1,8 @@ import fs from "node:fs/promises"; import os from "os"; import util from "util"; -import { Gauge } from "./prometheus"; -import { SystemInfo } from "./data"; +import { Gauge } from "./prometheus.js"; +import { SystemInfo } from "./data/index.js"; function filterCpuModel() { const model = os.cpus()[0].model; diff --git a/packages/lib/tsconfig.browser.json b/packages/lib/tsconfig.browser.json index 8a87bca4..f90faa99 100644 --- a/packages/lib/tsconfig.browser.json +++ b/packages/lib/tsconfig.browser.json @@ -15,6 +15,7 @@ "./src/helpers.ts", "./src/hash.ts", "./src/ini.ts", + "./src/is_deep_strict_equal.browser.ts", "./src/link/*.ts", "./src/lua_tools.ts", "./src/logging.ts", diff --git a/packages/lib/tsconfig.node.json b/packages/lib/tsconfig.node.json index ef6ccd30..983999cb 100644 --- a/packages/lib/tsconfig.node.json +++ b/packages/lib/tsconfig.node.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.node.json", "include": ["index.ts", "build_mod.js", "src/**/*.ts", "src/**/*.js"], + "exclude": ["src/**/*.browser.ts"], "compilerOptions": { "allowJs": true, }, diff --git a/packages/web_ui/webpack.common.js b/packages/web_ui/webpack.common.js index 26545081..2ae834ab 100644 --- a/packages/web_ui/webpack.common.js +++ b/packages/web_ui/webpack.common.js @@ -141,6 +141,9 @@ module.exports = (env = {}, argv = {}) => ({ }, resolve: { extensions: [".js", ".ts", ".json", ".jsx", ".tsx"], + extensionAlias: { + ".js": [".ts", ".js"], + }, fallback: { "crypto": false, "fs": false, diff --git a/plugins/global_chat/control.ts b/plugins/global_chat/control.ts index 3b6534ba..ff4f5068 100644 --- a/plugins/global_chat/control.ts +++ b/plugins/global_chat/control.ts @@ -1,6 +1,6 @@ import { CommandTree, Command } from "@clusterio/lib"; import { BaseCtlPlugin } from "@clusterio/ctl"; -import { ChatEvent } from "./messages"; +import { ChatEvent } from "./messages.js"; const globalChatCommands = new CommandTree({ name: "global-chat", description: "Global Chat plugin commands", diff --git a/plugins/global_chat/index.ts b/plugins/global_chat/index.ts index a02d5cfd..dee12820 100644 --- a/plugins/global_chat/index.ts +++ b/plugins/global_chat/index.ts @@ -1,13 +1,13 @@ import type * as lib from "@clusterio/lib"; -import { ChatEvent } from "./messages"; +import { ChatEvent } from "./messages.js"; export const plugin: lib.PluginDeclaration = { name: "global_chat", title: "Global Chat", description: "Forwards chat between instances.", - instanceEntrypoint: "dist/node/instance", - ctlEntrypoint: "dist/node/control", + instanceEntrypoint: "dist/node/instance.js", + ctlEntrypoint: "dist/node/control.js", features: [ "ScriptCommands", diff --git a/plugins/global_chat/instance.ts b/plugins/global_chat/instance.ts index 2a3fa4e6..22cf1dda 100644 --- a/plugins/global_chat/instance.ts +++ b/plugins/global_chat/instance.ts @@ -1,6 +1,6 @@ import * as lib from "@clusterio/lib"; import { BaseInstancePlugin } from "@clusterio/host"; -import { ChatEvent } from "./messages"; +import { ChatEvent } from "./messages.js"; /** * Removes server-specific tags from messages. diff --git a/plugins/global_chat/package.json b/plugins/global_chat/package.json index c33bba72..1b259dbb 100644 --- a/plugins/global_chat/package.json +++ b/plugins/global_chat/package.json @@ -23,6 +23,7 @@ "access": "public" }, + "type": "module", "main": "dist/node/index.js", "scripts": { "prepare": "tsc --build && webpack-cli --mode production", diff --git a/plugins/global_chat/test/plugin.js b/plugins/global_chat/test/plugin.js index 6436dc16..0b3b1a1e 100644 --- a/plugins/global_chat/test/plugin.js +++ b/plugins/global_chat/test/plugin.js @@ -1,13 +1,10 @@ -"use strict"; -const assert = require("assert").strict; -const lib = require("@clusterio/lib"); - -const mock = require("../../../test/mock"); -const lines = require("../../../test/lib/factorio/lines"); -const instance = require("../dist/node/instance"); -const info = require("../dist/node/index").plugin; -const { ChatEvent } = require("../dist/node/messages"); +import assert from "node:assert/strict"; +import * as mock from "../../../test/mock.js"; +import * as lines from "../../../test/lib/factorio/lines.js"; +import * as instance from "../dist/node/instance.js"; +import { plugin as info } from "../dist/node/index.js"; +import { ChatEvent } from "../dist/node/messages.js"; describe("global_chat plugin", function() { describe("removeTags()", function() { diff --git a/plugins/global_chat/webpack.config.js b/plugins/global_chat/webpack.config.cjs similarity index 100% rename from plugins/global_chat/webpack.config.js rename to plugins/global_chat/webpack.config.cjs diff --git a/plugins/inventory_sync/controller.ts b/plugins/inventory_sync/controller.ts index 19df1451..6bd7fa10 100644 --- a/plugins/inventory_sync/controller.ts +++ b/plugins/inventory_sync/controller.ts @@ -1,10 +1,10 @@ import { BaseControllerPlugin, type InstanceRecord } from "@clusterio/controller"; -import type { IpcPlayerData } from "./messages"; +import type { IpcPlayerData } from "./messages.js"; import fs from "node:fs/promises"; import path from "path"; import * as lib from "@clusterio/lib"; -import * as msg from "./messages"; +import * as msg from "./messages.js"; async function loadDatabase(config: lib.ControllerConfig, logger: lib.Logger) { let itemsPath = path.resolve(config.get("controller.database_directory"), "inventories.json"); diff --git a/plugins/inventory_sync/index.ts b/plugins/inventory_sync/index.ts index f38ec669..dc78621b 100644 --- a/plugins/inventory_sync/index.ts +++ b/plugins/inventory_sync/index.ts @@ -1,5 +1,5 @@ import * as lib from "@clusterio/lib"; -import * as messages from "./messages"; +import * as messages from "./messages.js"; declare module "@clusterio/lib" { export interface InstanceConfigFields { @@ -22,7 +22,7 @@ export const plugin: lib.PluginDeclaration = { title: "Inventory sync", description: "Synchronizes players inventories between instances", - instanceEntrypoint: "dist/node/instance", + instanceEntrypoint: "dist/node/instance.js", instanceConfigFields: { "inventory_sync.rcon_chunk_size": { title: "Rcon inventory chunk size", @@ -33,7 +33,7 @@ export const plugin: lib.PluginDeclaration = { }, }, - controllerEntrypoint: "dist/node/controller", + controllerEntrypoint: "dist/node/controller.js", controllerConfigFields: { "inventory_sync.player_lock_timeout": { title: "Player Lock Timeout", diff --git a/plugins/inventory_sync/instance.ts b/plugins/inventory_sync/instance.ts index c831e8fd..bc05d896 100644 --- a/plugins/inventory_sync/instance.ts +++ b/plugins/inventory_sync/instance.ts @@ -2,7 +2,7 @@ import * as lib from "@clusterio/lib"; import { BaseInstancePlugin } from "@clusterio/host"; import { AcquireRequest, AcquireResponse, ReleaseRequest, UploadRequest, DownloadRequest, DownloadResponse, IpcPlayerData, -} from "./messages"; +} from "./messages.js"; type IpcPlayerName = { player_name: string diff --git a/plugins/inventory_sync/package.json b/plugins/inventory_sync/package.json index 14c093a0..035df1ac 100644 --- a/plugins/inventory_sync/package.json +++ b/plugins/inventory_sync/package.json @@ -23,6 +23,7 @@ "access": "public" }, + "type": "module", "main": "dist/node/index.js", "scripts": { "prepare": "tsc --build && webpack-cli --mode production", diff --git a/plugins/inventory_sync/web/index.tsx b/plugins/inventory_sync/web/index.tsx index f17184e3..21aef86b 100644 --- a/plugins/inventory_sync/web/index.tsx +++ b/plugins/inventory_sync/web/index.tsx @@ -1,7 +1,7 @@ import React, { useContext, useEffect, useState } from "react"; import { BaseWebPlugin, PageLayout, PageHeader, ControlContext } from "@clusterio/web_ui"; -import { DatabaseStatsRequest, DatabaseStatsResponse } from "../messages"; +import { DatabaseStatsRequest, DatabaseStatsResponse } from "../messages.js"; import "./style.css"; diff --git a/plugins/inventory_sync/webpack.config.js b/plugins/inventory_sync/webpack.config.cjs similarity index 100% rename from plugins/inventory_sync/webpack.config.js rename to plugins/inventory_sync/webpack.config.cjs diff --git a/plugins/player_auth/controller.ts b/plugins/player_auth/controller.ts index 02e9c25a..19dcce81 100644 --- a/plugins/player_auth/controller.ts +++ b/plugins/player_auth/controller.ts @@ -4,11 +4,10 @@ import util from "util"; import jwt from "jsonwebtoken"; import { Static } from "@sinclair/typebox"; -import * as lib from "@clusterio/lib"; import { BaseControllerPlugin } from "@clusterio/controller"; -const { basicType } = lib; +import { basicType, RequestError } from "@clusterio/lib"; -import { FetchPlayerCodeRequest, PlayerAuthServer, SetVerifyCodeRequest } from "./messages"; +import { FetchPlayerCodeRequest, PlayerAuthServer, SetVerifyCodeRequest } from "./messages.js"; async function generateCode(length: number): Promise { @@ -207,7 +206,7 @@ export class ControllerPlugin extends BaseControllerPlugin { let entry = this.players.get(player); if (!entry || entry.expiresMs < Date.now()) { - throw new lib.RequestError("invalid player"); + throw new RequestError("invalid player"); } entry.verifyCode = verifyCode; diff --git a/plugins/player_auth/index.ts b/plugins/player_auth/index.ts index f8d860b1..f4f5d6aa 100644 --- a/plugins/player_auth/index.ts +++ b/plugins/player_auth/index.ts @@ -1,5 +1,5 @@ import * as lib from "@clusterio/lib"; -import * as messages from "./messages"; +import * as messages from "./messages.js"; declare module "@clusterio/lib" { export interface InstanceConfigFields { @@ -16,8 +16,8 @@ export const plugin: lib.PluginDeclaration = { name: "player_auth", title: "Player Auth", description: "Provides authentication to the cluster via logging into a Factorio server.", - controllerEntrypoint: "dist/node/controller", - instanceEntrypoint: "dist/node/instance", + controllerEntrypoint: "dist/node/controller.js", + instanceEntrypoint: "dist/node/instance.js", webEntrypoint: "./web", controllerConfigFields: { "player_auth.code_length": { diff --git a/plugins/player_auth/instance.ts b/plugins/player_auth/instance.ts index 780dd1a3..58be8518 100644 --- a/plugins/player_auth/instance.ts +++ b/plugins/player_auth/instance.ts @@ -1,6 +1,6 @@ import { BaseInstancePlugin } from "@clusterio/host"; -import { FetchPlayerCodeRequest, SetVerifyCodeRequest } from "./messages"; +import { FetchPlayerCodeRequest, SetVerifyCodeRequest } from "./messages.js"; type IpcPlayerAuth = { type: "open_dialog", diff --git a/plugins/player_auth/package.json b/plugins/player_auth/package.json index e674f239..20b174a5 100644 --- a/plugins/player_auth/package.json +++ b/plugins/player_auth/package.json @@ -23,6 +23,7 @@ "access": "public" }, + "type": "module", "main": "dist/node/index.js", "scripts": { "prepare": "tsc --build && webpack-cli --mode production", diff --git a/plugins/player_auth/test/plugin.js b/plugins/player_auth/test/plugin.js index 35d21e54..590af809 100644 --- a/plugins/player_auth/test/plugin.js +++ b/plugins/player_auth/test/plugin.js @@ -1,15 +1,14 @@ -"use strict"; -const assert = require("assert").strict; -const jwt = require("jsonwebtoken"); +import assert from "node:assert/strict"; +import jwt from "jsonwebtoken"; -const mock = require("../../../test/mock"); +import * as mock from "../../../test/mock.js"; -const controller = require("../dist/node/controller"); -const instance = require("../dist/node/instance"); -const info = require("../dist/node/index").plugin; -const { FetchPlayerCodeRequest, SetVerifyCodeRequest, PlayerAuthServer } = require("../dist/node/messages"); -const { testRoundTripJsonSerialisable, testMatrix } = require("../../../test/common"); -const lib = require("@clusterio/lib"); +import * as controller from "../dist/node/controller.js"; +import * as instance from "../dist/node/instance.js"; +import { plugin as info } from "../dist/node/index.js"; +import { FetchPlayerCodeRequest, SetVerifyCodeRequest, PlayerAuthServer } from "../dist/node/messages.js"; +import { testRoundTripJsonSerialisable, testMatrix } from "../../../test/common.js"; +import * as lib from "@clusterio/lib"; function postJSON(url, body) { diff --git a/plugins/player_auth/web/index.tsx b/plugins/player_auth/web/index.tsx index 1c5cd20a..17cb7531 100644 --- a/plugins/player_auth/web/index.tsx +++ b/plugins/player_auth/web/index.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useState } from "react"; import { BaseWebPlugin, notifyErrorHandler } from "@clusterio/web_ui"; import { Alert, Button, Form, Input, Modal, Space, Spin, Typography } from "antd"; -import { PlayerAuthServer } from "../messages"; +import { PlayerAuthServer } from "../messages.js"; import "./style.css"; const { Paragraph, Text } = Typography; diff --git a/plugins/player_auth/webpack.config.js b/plugins/player_auth/webpack.config.cjs similarity index 100% rename from plugins/player_auth/webpack.config.js rename to plugins/player_auth/webpack.config.cjs diff --git a/plugins/research_sync/controller.ts b/plugins/research_sync/controller.ts index 498e2a45..954e5f70 100644 --- a/plugins/research_sync/controller.ts +++ b/plugins/research_sync/controller.ts @@ -4,7 +4,7 @@ import { BaseControllerPlugin } from "@clusterio/controller"; import { Static } from "@sinclair/typebox"; import * as lib from "@clusterio/lib"; -const { RateLimiter } = lib; +import { RateLimiter } from "@clusterio/lib"; import { ContributionEvent, @@ -13,7 +13,7 @@ import { TechnologySync, SyncTechnologiesRequest, TechnologyProgress, -} from "./messages"; +} from "./messages.js"; type Technology = { level: number, diff --git a/plugins/research_sync/index.ts b/plugins/research_sync/index.ts index 85afabdc..33a0967a 100644 --- a/plugins/research_sync/index.ts +++ b/plugins/research_sync/index.ts @@ -1,13 +1,13 @@ import type * as lib from "@clusterio/lib"; -import * as messages from "./messages"; +import * as messages from "./messages.js"; export const plugin: lib.PluginDeclaration = { name: "research_sync", title: "Research Sync", description: "Synchronises technology research progress between instances.", - instanceEntrypoint: "dist/node/instance", - controllerEntrypoint: "dist/node/controller", + instanceEntrypoint: "dist/node/instance.js", + controllerEntrypoint: "dist/node/controller.js", features: [ "SavePatching", diff --git a/plugins/research_sync/instance.ts b/plugins/research_sync/instance.ts index 5ce28907..5e254d86 100644 --- a/plugins/research_sync/instance.ts +++ b/plugins/research_sync/instance.ts @@ -6,7 +6,7 @@ import { FinishedEvent, TechnologySync, SyncTechnologiesRequest, -} from "./messages"; +} from "./messages.js"; // ./module/sync.lua type IpcContribution = { diff --git a/plugins/research_sync/package.json b/plugins/research_sync/package.json index b21ebf5f..2f3ef9ea 100644 --- a/plugins/research_sync/package.json +++ b/plugins/research_sync/package.json @@ -23,6 +23,7 @@ "access": "public" }, + "type": "module", "main": "dist/node/index.js", "scripts": { "prepare": "tsc --build && webpack-cli --mode production", diff --git a/plugins/research_sync/test/missing.js b/plugins/research_sync/test/missing.js index ae16e8f1..4d585e2b 100644 --- a/plugins/research_sync/test/missing.js +++ b/plugins/research_sync/test/missing.js @@ -1,6 +1,5 @@ // These are libraries that does not have any units test, they are // required here in order to track coverage for them. -"use strict"; -require("../dist/node/instance"); -require("../dist/node/controller"); +import "../dist/node/instance.js"; +import "../dist/node/controller.js"; diff --git a/plugins/research_sync/webpack.config.js b/plugins/research_sync/webpack.config.cjs similarity index 100% rename from plugins/research_sync/webpack.config.js rename to plugins/research_sync/webpack.config.cjs diff --git a/plugins/statistics_exporter/index.ts b/plugins/statistics_exporter/index.ts index 6e769973..f9a38150 100644 --- a/plugins/statistics_exporter/index.ts +++ b/plugins/statistics_exporter/index.ts @@ -12,7 +12,7 @@ export const plugin: lib.PluginDeclaration = { description: "Provides in-game item/fluid production, builds, kills, and pollution "+ "statistics to the cluster's Prometheus endpoint.", - instanceEntrypoint: "dist/node/instance", + instanceEntrypoint: "dist/node/instance.js", instanceConfigFields: { "statistics_exporter.command_timeout": { title: "Command Timeout", diff --git a/plugins/statistics_exporter/instance.ts b/plugins/statistics_exporter/instance.ts index c76bf1f0..82382ada 100644 --- a/plugins/statistics_exporter/instance.ts +++ b/plugins/statistics_exporter/instance.ts @@ -1,7 +1,7 @@ import * as lib from "@clusterio/lib"; import { BaseInstancePlugin } from "@clusterio/host"; -const { Gauge } = lib; +import { Gauge } from "@clusterio/lib"; type IpcStats = { game_tick: number, diff --git a/plugins/statistics_exporter/package.json b/plugins/statistics_exporter/package.json index 68c2a4a0..5ca2bb5a 100644 --- a/plugins/statistics_exporter/package.json +++ b/plugins/statistics_exporter/package.json @@ -23,6 +23,7 @@ "access": "public" }, + "type": "module", "main": "dist/node/index.js", "scripts": { "prepare": "tsc --build && webpack-cli --mode production", diff --git a/plugins/statistics_exporter/test/plugin.js b/plugins/statistics_exporter/test/plugin.js index 6c0393e1..49f1439e 100644 --- a/plugins/statistics_exporter/test/plugin.js +++ b/plugins/statistics_exporter/test/plugin.js @@ -1,9 +1,8 @@ -"use strict"; -const assert = require("assert").strict; +import assert from "node:assert/strict"; -const mock = require("../../../test/mock"); -const instance = require("../dist/node/instance"); -const info = require("../dist/node/index").plugin; +import * as mock from "../../../test/mock.js"; +import * as instance from "../dist/node/instance.js"; +import { plugin as info } from "../dist/node/index.js"; describe("statistics_exporter plugin", function() { diff --git a/plugins/statistics_exporter/webpack.config.js b/plugins/statistics_exporter/webpack.config.cjs similarity index 100% rename from plugins/statistics_exporter/webpack.config.js rename to plugins/statistics_exporter/webpack.config.cjs diff --git a/plugins/subspace_storage/controller.ts b/plugins/subspace_storage/controller.ts index 4630d1a5..32f990cc 100644 --- a/plugins/subspace_storage/controller.ts +++ b/plugins/subspace_storage/controller.ts @@ -4,10 +4,10 @@ import fs from "node:fs/promises"; import path from "path"; import * as lib from "@clusterio/lib"; -const { Counter, Gauge } = lib; +import { Counter, Gauge } from "@clusterio/lib"; -import * as routes from "./routes"; -import * as dole from "./dole"; +import * as routes from "./routes.js"; +import * as dole from "./dole.js"; import { Item, @@ -16,7 +16,7 @@ import { GetStorageRequest, UpdateStorageEvent, SetStorageSubscriptionRequest, -} from "./messages"; +} from "./messages.js"; const exportCounter = new Counter( diff --git a/plugins/subspace_storage/dole.ts b/plugins/subspace_storage/dole.ts index 4060f424..f01f20e9 100644 --- a/plugins/subspace_storage/dole.ts +++ b/plugins/subspace_storage/dole.ts @@ -1,7 +1,7 @@ -import * as doleNN from "./dole_nn_base"; +import * as doleNN from "./dole_nn_base.js"; import * as lib from "@clusterio/lib"; -const { Gauge } = lib; +import { Gauge } from "@clusterio/lib"; const prometheusNNDoleGauge = new Gauge( diff --git a/plugins/subspace_storage/index.ts b/plugins/subspace_storage/index.ts index 300c8895..00cb5914 100644 --- a/plugins/subspace_storage/index.ts +++ b/plugins/subspace_storage/index.ts @@ -1,5 +1,5 @@ import * as lib from "@clusterio/lib"; -import * as messages from "./messages"; +import * as messages from "./messages.js"; declare module "@clusterio/lib" { export interface ControllerConfigFields { @@ -22,7 +22,7 @@ export const plugin: lib.PluginDeclaration = { name: "subspace_storage", title: "Subspace Storage", description: "Provides shared storage across instances for the Subspace Storage mod", - instanceEntrypoint: "dist/node/instance", + instanceEntrypoint: "dist/node/instance.js", instanceConfigFields: { "subspace_storage.log_item_transfers": { title: "Log Item Transfers", @@ -32,7 +32,7 @@ export const plugin: lib.PluginDeclaration = { }, }, - controllerEntrypoint: "dist/node/controller", + controllerEntrypoint: "dist/node/controller.js", controllerConfigFields: { "subspace_storage.division_method": { title: "Division Method", diff --git a/plugins/subspace_storage/instance.ts b/plugins/subspace_storage/instance.ts index 4de15afb..87ab957f 100644 --- a/plugins/subspace_storage/instance.ts +++ b/plugins/subspace_storage/instance.ts @@ -7,7 +7,7 @@ import { GetStorageRequest, UpdateStorageEvent, Item, -} from "./messages"; +} from "./messages.js"; type IpcItems = [name: string, count: number, quality: string][]; diff --git a/plugins/subspace_storage/package.json b/plugins/subspace_storage/package.json index 3b77dec0..7a7c2582 100644 --- a/plugins/subspace_storage/package.json +++ b/plugins/subspace_storage/package.json @@ -23,6 +23,7 @@ "access": "public" }, + "type": "module", "main": "dist/node/index.js", "scripts": { "prepare": "tsc --build && webpack-cli --mode production", diff --git a/plugins/subspace_storage/test/missing.js b/plugins/subspace_storage/test/missing.js index ab3009d7..e01532ac 100644 --- a/plugins/subspace_storage/test/missing.js +++ b/plugins/subspace_storage/test/missing.js @@ -1,8 +1,7 @@ // These are libraries that does not have any units test, they are // required here in order to track coverage for them. -"use strict"; -require("../dist/node/instance"); -require("../dist/node/controller"); -require("../dist/node/dole"); -require("../dist/node/dole_nn_base"); +import "../dist/node/instance.js"; +import "../dist/node/controller.js"; +import "../dist/node/dole.js"; +import "../dist/node/dole_nn_base.js"; diff --git a/plugins/subspace_storage/test/routes.js b/plugins/subspace_storage/test/routes.js index fc4aac5b..3d8b0afd 100644 --- a/plugins/subspace_storage/test/routes.js +++ b/plugins/subspace_storage/test/routes.js @@ -1,8 +1,7 @@ -"use strict"; -const assert = require("assert").strict; +import assert from "node:assert/strict"; -const routes = require("../dist/node/routes"); -const { get } = require("../../../test/integration"); +import * as routes from "../dist/node/routes.js"; +import { get } from "../../../test/integration/index.js"; describe("subspace_storage endpoints", function() { diff --git a/plugins/subspace_storage/web/index.tsx b/plugins/subspace_storage/web/index.tsx index 2ead807c..2b91ca61 100644 --- a/plugins/subspace_storage/web/index.tsx +++ b/plugins/subspace_storage/web/index.tsx @@ -6,7 +6,7 @@ import { useExportLocale, useExportPrototypeMetadata, useDefaultModPack, FactorioIcon, useTableQueryState, useColumnSearch, } from "@clusterio/web_ui"; -import { GetStorageRequest, Item, SetStorageSubscriptionRequest, UpdateStorageEvent } from "../messages"; +import { GetStorageRequest, Item, SetStorageSubscriptionRequest, UpdateStorageEvent } from "../messages.js"; import "./style.css"; diff --git a/plugins/subspace_storage/webpack.config.js b/plugins/subspace_storage/webpack.config.cjs similarity index 100% rename from plugins/subspace_storage/webpack.config.js rename to plugins/subspace_storage/webpack.config.cjs diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f22b099c..949418cc 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -11,7 +11,7 @@ catalog: '@types/express': ^4.17.17 '@types/finalhandler': ^1.2.0 '@types/jsonwebtoken': ^9.0.9 - '@types/node': ^20.17.19 + '@types/node': ^22.20.1 '@types/react': ^18.3.18 '@types/set-blocking': ^2.0.0 '@types/ws': ^8.5.14 @@ -38,4 +38,7 @@ catalogMode: strict linkWorkspacePackages: true +onlyBuiltDependencies: + - '@swc/core' + shellEmulator: true diff --git a/publish.js b/publish.js index 80c81a7c..90aaabaf 100644 --- a/publish.js +++ b/publish.js @@ -2,10 +2,9 @@ * Publish any mods that need to be published */ -"use strict"; -const FormData = require("form-data"); -const fs = require("node:fs/promises"); -const yargs = require("yargs"); +import FormData from "form-data"; +import fs from "node:fs/promises"; +import yargs from "yargs"; let DRY = false; @@ -129,6 +128,6 @@ async function main() { } // Run main if started from command line -if (module === require.main) { +if (import.meta.main) { main(); } diff --git a/scripts/version-compat-test.js b/scripts/version-compat-test.js index d4055900..1e7c9250 100644 --- a/scripts/version-compat-test.js +++ b/scripts/version-compat-test.js @@ -19,16 +19,16 @@ * The repository must be built (pnpm run build) before running with a local side. */ "use strict"; -const child_process = require("child_process"); -const events = require("events"); -const fs = require("fs/promises"); -const path = require("path"); -const readline = require("readline"); -const util = require("util"); +import child_process from "node:child_process"; +import events from "node:events"; +import fs from "node:fs/promises"; +import path from "node:path"; +import readline from "node:readline"; +import util from "node:util"; const execFile = util.promisify(child_process.execFile); -const repoRoot = path.join(__dirname, ".."); +const repoRoot = path.join(import.meta.dirname, ".."); const workDir = path.join(repoRoot, "temp", "compat-test"); const controllerDir = path.join(workDir, "controller"); const hostDir = path.join(workDir, "host"); diff --git a/test/common.js b/test/common.js index eb75a3f2..b35116d9 100644 --- a/test/common.js +++ b/test/common.js @@ -1,6 +1,5 @@ -"use strict"; -const assert = require("assert").strict; -const { compile } = require("@clusterio/lib"); +import assert from "node:assert/strict"; +import { compile } from "@clusterio/lib"; /** * Generate a flat array of tests from a matrix of inputs. @@ -10,7 +9,7 @@ const { compile } = require("@clusterio/lib"); * @returns {Array<{ [K in keyof T]: T[K][number] }>} * An array of tuples, each containing one value from each input array. */ -function testMatrix(...arrays) { +export function testMatrix(...arrays) { return arrays.reduce((acc, curr) => acc.flatMap(a => curr.map(b => [...a, b])), [[]]); } @@ -21,7 +20,7 @@ function testMatrix(...arrays) { * @param {{new(...args: T): object}} Class - The class which has toJSON and fromJSON methods. * @param {T[]} tests - The tests inputs to pass to the class constructor. */ -function testRoundTripJsonSerialisable(Class, tests) { +export function testRoundTripJsonSerialisable(Class, tests) { const validate = compile(Class.jsonSchema); for (const test of tests) { const original = new Class(...test); @@ -34,8 +33,3 @@ function testRoundTripJsonSerialisable(Class, tests) { } } } - -module.exports = { - testMatrix, - testRoundTripJsonSerialisable, -}; diff --git a/test/controller/BaseControllerPlugin.js b/test/controller/BaseControllerPlugin.js index b4dda949..0ff45811 100644 --- a/test/controller/BaseControllerPlugin.js +++ b/test/controller/BaseControllerPlugin.js @@ -1,6 +1,5 @@ -"use strict"; -const mock = require("../mock"); -const controller = require("@clusterio/controller"); +import * as mock from "../mock.js"; +import * as controller from "@clusterio/controller"; describe("controller/src/BaseControllerPlugin", function() { describe("class BaseControllerPlugin", function() { diff --git a/test/controller/ControlConnection.test.js b/test/controller/ControlConnection.test.js index 90483a08..2ad0db9d 100644 --- a/test/controller/ControlConnection.test.js +++ b/test/controller/ControlConnection.test.js @@ -1,6 +1,5 @@ -"use strict"; -const assert = require("assert").strict; -const { ControlConnection } = require("@clusterio/controller"); +import assert from "node:assert/strict"; +import { ControlConnection } from "@clusterio/controller"; describe("controller/src/ControlConnection", function() { describe(".handleControllerRestartRequest()", function() { diff --git a/test/controller/Controller.js b/test/controller/Controller.js index 6054854a..d2f014ca 100644 --- a/test/controller/Controller.js +++ b/test/controller/Controller.js @@ -1,17 +1,16 @@ -"use strict"; -const path = require("node:path"); -const assert = require("assert").strict; -const { Controller, HostRecord, InstanceRecord, UserRecord } = require("@clusterio/controller"); -const { EventEmitter } = require("stream"); -const events = require("node:events"); -const http = require("node:http"); -const express = require("express"); +import path from "node:path"; +import assert from "node:assert/strict"; +import { Controller, HostRecord, InstanceRecord, UserRecord } from "@clusterio/controller"; +import events, { EventEmitter } from "node:events"; +import http from "node:http"; +import express from "express"; +import { fileURLToPath } from "node:url"; -const { +import { ControllerConfig, Address, RequestError, InstanceConfig, SystemInfo, Role, ModPack, addPluginConfigFields, -} = require("@clusterio/lib"); +} from "@clusterio/lib"; class MockEvent {} @@ -33,10 +32,12 @@ class MockInstanceConfig extends EventEmitter { } describe("controller/src/Controller", function() { - describe("class Controller", function() { + describe("class Controller", async function() { /** @type {Controller} */ let controller, mockInstanceConfig; - const controllerVersion = require("@clusterio/controller/package.json").version; + const controllerVersion = ( + await import("@clusterio/controller/package.json", { with: { type: "json" }}) + ).default.version; before(async function() { const controllerConfig = new ControllerConfig("controller", { "controller.version": controllerVersion }); controller = new Controller({}, [], controllerConfig); @@ -245,7 +246,7 @@ describe("controller/src/Controller", function() { controller.config.restartRequired = false; controller.pluginInfos[0] = { name: "restart_test", - packagePath: require.resolve("@clusterio/controller/package.json"), + packagePath: fileURLToPath(import.meta.resolve("@clusterio/controller/package.json")), version: controllerVersion, }; }); diff --git a/test/controller/ControllerUser.js b/test/controller/ControllerUser.js index 12f789c3..8897e474 100644 --- a/test/controller/ControllerUser.js +++ b/test/controller/ControllerUser.js @@ -1,7 +1,6 @@ -"use strict"; -const assert = require("assert").strict; -const lib = require("@clusterio/lib"); -const { User } = require("@clusterio/controller"); +import assert from "node:assert/strict"; +import * as lib from "@clusterio/lib"; +import { User } from "@clusterio/controller"; describe("controller/src/User", function() { describe("class User", function() { diff --git a/test/controller/HostRecord.test.js b/test/controller/HostRecord.test.js index 60ca74de..687497e4 100644 --- a/test/controller/HostRecord.test.js +++ b/test/controller/HostRecord.test.js @@ -1,9 +1,8 @@ -"use strict"; -const assert = require("assert").strict; -const { HostDetails } = require("@clusterio/lib"); -const { HostRecord } = require("@clusterio/controller"); +import assert from "node:assert/strict"; +import { HostDetails } from "@clusterio/lib"; +import { HostRecord } from "@clusterio/controller"; -const { testMatrix, testRoundTripJsonSerialisable } = require("../common"); +import { testMatrix, testRoundTripJsonSerialisable } from "../common.js"; describe("controller/HostRecord", function () { const samplePlugins = new Map([ diff --git a/test/controller/HttpCloser.js b/test/controller/HttpCloser.js index d7bcadd4..f072d051 100644 --- a/test/controller/HttpCloser.js +++ b/test/controller/HttpCloser.js @@ -1,12 +1,11 @@ -"use strict"; -const assert = require("assert").strict; -const fs = require("node:fs/promises"); -const http = require("http"); -const https = require("https"); -const util = require("util"); -const events = require("node:events"); +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import http from "node:http"; +import https from "node:https"; +import util from "node:util"; +import events from "node:events"; -const HttpCloser = require("@clusterio/controller/dist/node/src/HttpCloser").default; +import HttpCloser from "@clusterio/controller/dist/node/src/HttpCloser.js"; // Time to wait during async operations to ensure they happened in order. const tick = 20; diff --git a/test/controller/InstanceManager.test.js b/test/controller/InstanceManager.test.js index 312cb402..59e05063 100644 --- a/test/controller/InstanceManager.test.js +++ b/test/controller/InstanceManager.test.js @@ -1,8 +1,7 @@ -"use strict"; -const assert = require("assert").strict; -const lib = require("@clusterio/lib"); +import assert from "node:assert/strict"; +import * as lib from "@clusterio/lib"; -const { Controller, InstanceRecord, InstanceManager } = require("@clusterio/controller"); +import { Controller, InstanceRecord, InstanceManager } from "@clusterio/controller"; describe("controller/InstanceManager", function () { /** @type {Controller} */ diff --git a/test/controller/InstanceRecord.test.js b/test/controller/InstanceRecord.test.js index c3c169c5..7b496bad 100644 --- a/test/controller/InstanceRecord.test.js +++ b/test/controller/InstanceRecord.test.js @@ -1,9 +1,8 @@ -"use strict"; -const assert = require("assert").strict; -const lib = require("@clusterio/lib"); -const { InstanceRecord } = require("@clusterio/controller"); +import assert from "node:assert/strict"; +import * as lib from "@clusterio/lib"; +import { InstanceRecord } from "@clusterio/controller"; -const { testMatrix, testRoundTripJsonSerialisable } = require("../common"); +import { testMatrix, testRoundTripJsonSerialisable } from "../common.js"; describe("controller/InstanceRecord", function () { /** @type {lib.InstanceConfig} */ diff --git a/test/controller/User.test.js b/test/controller/User.test.js index cf605280..a04bbdc0 100644 --- a/test/controller/User.test.js +++ b/test/controller/User.test.js @@ -1,7 +1,6 @@ -"use strict"; -const assert = require("assert").strict; -const { Role, SubscribableDatastore } = require("@clusterio/lib"); -const { User } = require("@clusterio/controller"); +import assert from "node:assert/strict"; +import { Role, SubscribableDatastore } from "@clusterio/lib"; +import { User } from "@clusterio/controller"; describe("controller/User", function () { /** @type {SubscribableDatastore} */ diff --git a/test/controller/UserManager.test.js b/test/controller/UserManager.test.js index 8312c3b7..5005875e 100644 --- a/test/controller/UserManager.test.js +++ b/test/controller/UserManager.test.js @@ -1,10 +1,9 @@ -"use strict"; -const path = require("path"); -const fs = require("node:fs/promises"); -const assert = require("assert").strict; -const jwt = require("jsonwebtoken"); -const { Role, SubscribableDatastore, ControllerConfig } = require("@clusterio/lib"); -const { UserRecord, UserManager } = require("@clusterio/controller"); +import path from "node:path"; +import fs from "node:fs/promises"; +import assert from "node:assert/strict"; +import jwt from "jsonwebtoken"; +import { Role, SubscribableDatastore, ControllerConfig } from "@clusterio/lib"; +import { UserRecord, UserManager } from "@clusterio/controller"; const TEMP_DIR = path.join("temp", "test", "UserManager"); diff --git a/test/controller/UserRecord.test.js b/test/controller/UserRecord.test.js index fe7c6ccb..c4e2eee9 100644 --- a/test/controller/UserRecord.test.js +++ b/test/controller/UserRecord.test.js @@ -1,9 +1,8 @@ -"use strict"; -const assert = require("assert").strict; -const { UserDetails, PlayerStats } = require("@clusterio/lib"); -const { UserRecord } = require("@clusterio/controller"); +import assert from "node:assert/strict"; +import { UserDetails, PlayerStats } from "@clusterio/lib"; +import { UserRecord } from "@clusterio/controller"; -const { testMatrix, testRoundTripJsonSerialisable } = require("../common"); +import { testMatrix, testRoundTripJsonSerialisable } from "../common.js"; describe("controller/UserRecord", function () { it("should be round trip serialisable", function () { diff --git a/test/controller/WsServer.js b/test/controller/WsServer.js index 7501ec59..3b97fe40 100644 --- a/test/controller/WsServer.js +++ b/test/controller/WsServer.js @@ -1,7 +1,6 @@ -"use strict"; -const assert = require("assert").strict; -const { Controller, WsServer } = require("@clusterio/controller"); -const { ControllerConfig } = require("@clusterio/lib"); +import assert from "node:assert/strict"; +import { Controller, WsServer } from "@clusterio/controller"; +import { ControllerConfig } from "@clusterio/lib"; describe("controller/src/WsServer", function() { describe("class WsServer", function() { diff --git a/test/controller/routes.js b/test/controller/routes.js index 5087e97b..5e764214 100644 --- a/test/controller/routes.js +++ b/test/controller/routes.js @@ -1,12 +1,11 @@ -"use strict"; -const assert = require("assert").strict; -const events = require("events"); -const FormData = require("form-data"); -const http = require("http"); +import assert from "node:assert/strict"; +import events from "node:events"; +import FormData from "form-data"; +import http from "node:http"; -const { wait } = require("@clusterio/lib"); -const routes = require("@clusterio/controller/dist/node/src/routes"); -const mock = require("../mock"); +import { wait } from "@clusterio/lib"; +import * as routes from "@clusterio/controller/dist/node/src/routes.js"; +import * as mock from "../mock.js"; describe("controller/src/routes", function() { let controller; diff --git a/test/create/echo_args.js b/test/create/echo_args.js index 1319b1cf..ecc1f2a1 100644 --- a/test/create/echo_args.js +++ b/test/create/echo_args.js @@ -1,4 +1,3 @@ -"use strict"; -if (module === require.main) { +if (import.meta.main) { process.stdout.write(JSON.stringify(process.argv.slice(2))); } diff --git a/test/create/escape_arg.js b/test/create/escape_arg.js index 593b1a3f..e9fcdf5f 100644 --- a/test/create/escape_arg.js +++ b/test/create/escape_arg.js @@ -1,17 +1,16 @@ -"use strict"; -const child_process = require("child_process"); -const util = require("util"); -const assert = require("assert").strict; -const path = require("path"); +import child_process from "node:child_process"; +import util from "node:util"; +import assert from "node:assert/strict"; +import path from "node:path"; -const { escapeArg } = require("../../packages/create/escape_arg"); +import { escapeArg } from "../../packages/create/escape_arg.js"; const execFile = util.promisify(child_process.execFile); async function exec(file, args) { const command = [file, ...args.map(escapeArg)].join(" "); const { stdout, stderr } = await execFile( command, - { shell: true, cwd: __dirname, env: { ...process.env, pct: "%"} } + { shell: true, cwd: import.meta.dirname, env: { ...process.env, pct: "%"} } ); return { stdout: JSON.parse(stdout), stderr }; } diff --git a/test/file/test_plugin/control.js b/test/file/test_plugin/control.js index 25a80a0c..6f903c05 100644 --- a/test/file/test_plugin/control.js +++ b/test/file/test_plugin/control.js @@ -1,6 +1,5 @@ -"use strict"; -const { Command, CommandTree } = require("@clusterio/lib"); -const { BaseCtlPlugin } = require("@clusterio/ctl"); +import { Command, CommandTree } from "@clusterio/lib"; +import { BaseCtlPlugin } from "@clusterio/ctl"; const testPluginCommands = new CommandTree({ name: "test-plugin", description: "Test plugin commands", @@ -15,12 +14,8 @@ testPluginCommands.add(new Command({ }, })); -class CtlPlugin extends BaseCtlPlugin { +export class CtlPlugin extends BaseCtlPlugin { async addCommands(rootCommand) { rootCommand.add(testPluginCommands); } } - -module.exports = { - CtlPlugin, -}; diff --git a/test/file/test_plugin/controller.js b/test/file/test_plugin/controller.js index 01988f39..2764aed2 100644 --- a/test/file/test_plugin/controller.js +++ b/test/file/test_plugin/controller.js @@ -1,9 +1,8 @@ -"use strict"; -const { BaseControllerPlugin } = require("@clusterio/controller"); +import { BaseControllerPlugin } from "@clusterio/controller"; -const { ControllerEcho, HostEcho } = require("./messages"); +import { ControllerEcho, HostEcho } from "./messages.js"; -class ControllerPlugin extends BaseControllerPlugin { +export class ControllerPlugin extends BaseControllerPlugin { async init() { this.logger.info("test_plugin controller loaded"); this.controller.handle(ControllerEcho, this.handleControllerEcho.bind(this)); @@ -15,7 +14,3 @@ class ControllerPlugin extends BaseControllerPlugin { return request.text; } } - -module.exports = { - ControllerPlugin, -}; diff --git a/test/file/test_plugin/host.js b/test/file/test_plugin/host.js index ca6a3bad..648d8c95 100644 --- a/test/file/test_plugin/host.js +++ b/test/file/test_plugin/host.js @@ -1,9 +1,8 @@ -"use strict"; -const { BaseHostPlugin } = require("@clusterio/host"); +import { BaseHostPlugin } from "@clusterio/host"; -const { HostEcho, HostEchoReceived } = require("./messages"); +import { HostEcho, HostEchoReceived } from "./messages.js"; -class HostPlugin extends BaseHostPlugin { +export class HostPlugin extends BaseHostPlugin { async init() { this.logger.info("test_plugin host loaded"); this.receivedEchoes = new Set(); @@ -20,7 +19,3 @@ class HostPlugin extends BaseHostPlugin { return this.receivedEchoes.has(request.text); } } - -module.exports = { - HostPlugin, -}; diff --git a/test/file/test_plugin/index.js b/test/file/test_plugin/index.js index 7938c45b..de9f64e2 100644 --- a/test/file/test_plugin/index.js +++ b/test/file/test_plugin/index.js @@ -1,24 +1,21 @@ -"use strict"; // Plugin exercising the plugin system in the integration tests, see #342. // Written as plain JavaScript so that it does not need to be built. -const { ControllerEcho, HostEcho, HostEchoReceived } = require("./messages"); +import { ControllerEcho, HostEcho, HostEchoReceived } from "./messages.js"; -module.exports = { - plugin: { - name: "test_plugin", - title: "Test Plugin", - description: "Plugin used to test the plugin system.", +export const plugin = { + name: "test_plugin", + title: "Test Plugin", + description: "Plugin used to test the plugin system.", - controllerEntrypoint: "controller", - hostEntrypoint: "host", - instanceEntrypoint: "instance", - ctlEntrypoint: "control", + controllerEntrypoint: "controller.js", + hostEntrypoint: "host.js", + instanceEntrypoint: "instance.js", + ctlEntrypoint: "control.js", - messages: [ - ControllerEcho, - HostEcho, - HostEchoReceived, - ], - }, + messages: [ + ControllerEcho, + HostEcho, + HostEchoReceived, + ], }; diff --git a/test/file/test_plugin/instance.js b/test/file/test_plugin/instance.js index 5296c026..61ec4c5f 100644 --- a/test/file/test_plugin/instance.js +++ b/test/file/test_plugin/instance.js @@ -1,12 +1,7 @@ -"use strict"; -const { BaseInstancePlugin } = require("@clusterio/host"); +import { BaseInstancePlugin } from "@clusterio/host"; -class InstancePlugin extends BaseInstancePlugin { +export class InstancePlugin extends BaseInstancePlugin { async init() { this.logger.info("test_plugin instance loaded"); } } - -module.exports = { - InstancePlugin, -}; diff --git a/test/file/test_plugin/messages.js b/test/file/test_plugin/messages.js index be98c2c4..59baf5af 100644 --- a/test/file/test_plugin/messages.js +++ b/test/file/test_plugin/messages.js @@ -1,9 +1,8 @@ -"use strict"; -const { JsonBoolean, JsonString } = require("@clusterio/lib"); +import { JsonBoolean, JsonString } from "@clusterio/lib"; // Sent from ctl to the controller, which answers with the text it was given // and broadcasts a HostEcho to the hosts. -class ControllerEcho { +export class ControllerEcho { static type = "request"; static src = "control"; static dst = "controller"; @@ -21,7 +20,7 @@ class ControllerEcho { } // Broadcast from the controller to the hosts by broadcastEventToHosts. -class HostEcho { +export class HostEcho { static type = "event"; static src = "controller"; static dst = "host"; @@ -39,7 +38,7 @@ class HostEcho { // Sent from ctl to a host, which answers with whether it received the given // text via a HostEcho. -class HostEchoReceived { +export class HostEchoReceived { static type = "request"; static src = "control"; static dst = "host"; @@ -55,9 +54,3 @@ class HostEchoReceived { toJSON() { return this.text; } static fromJSON(json) { return new this(json); } } - -module.exports = { - ControllerEcho, - HostEcho, - HostEchoReceived, -}; diff --git a/test/file/test_plugin/package.json b/test/file/test_plugin/package.json index 16079ed7..d3a552d8 100644 --- a/test/file/test_plugin/package.json +++ b/test/file/test_plugin/package.json @@ -3,6 +3,7 @@ "version": "0.0.1", "description": "Plugin used to test the plugin system, see #342", "private": true, + "type": "module", "main": "index.js", "keywords": [ "clusterio-plugin" diff --git a/test/host.js b/test/host.js index 5fb6ab5f..4fafc52e 100644 --- a/test/host.js +++ b/test/host.js @@ -1,12 +1,11 @@ -"use strict"; -const assert = require("assert").strict; -const path = require("path"); +import assert from "node:assert/strict"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; -const lib = require("@clusterio/lib"); -const Host = require("@clusterio/host/dist/node/src/Host").default; -const Instance = require("@clusterio/host/dist/node/src/Instance").default; -const { _discoverInstances } = require("@clusterio/host/dist/node/src/Host"); -const { HostConnector } = require("@clusterio/host/dist/node/host"); +import * as lib from "@clusterio/lib"; +import Host, { _discoverInstances } from "@clusterio/host/dist/node/src/Host.js"; +import Instance from "@clusterio/host/dist/node/src/Instance.js"; +import { HostConnector } from "@clusterio/host/dist/node/host.js"; describe("Host testing", function() { describe("discoverInstances()", function() { @@ -116,9 +115,11 @@ describe("Host testing", function() { ]); }); }); - describe(".checkRestartRequired()", function() { + describe(".checkRestartRequired()", async function() { let host; - const hostVersion = require("@clusterio/host/package.json").version; + const hostVersion = ( + await import("@clusterio/host/package.json", { with: { type: "json" }}) + ).default.version; before(function() { lib.addPluginConfigFields([{ name: "restart_test", hostEntrypoint: "host.js" }]); }); @@ -126,7 +127,7 @@ describe("Host testing", function() { // This can be used more generally, but i did not want to interfere with handleSyncUserListsEvent const pluginInfos = [{ name: "restart_test", - packagePath: require.resolve("@clusterio/host/package.json"), + packagePath: fileURLToPath(import.meta.resolve("@clusterio/host/package.json")), version: hostVersion, }]; const hostConfig = new lib.HostConfig("host", { "host.version": hostVersion }); @@ -177,9 +178,11 @@ describe("Host testing", function() { assert.equal(host.config.restartRequired, true); }); }); - describe(".checkRestartDowngrade()", function() { + describe(".checkRestartDowngrade()", async function() { let host; - const hostVersion = require("@clusterio/host/package.json").version; + const hostVersion = ( + await import("@clusterio/host/package.json", { with: { type: "json" }}) + ).default.version; beforeEach(function() { const hostConfig = new lib.HostConfig("host", { "host.version": hostVersion }); const hostConnector = new HostConnector(hostConfig, []); diff --git a/test/host/BaseHostPlugin.js b/test/host/BaseHostPlugin.js index a2e15739..0394f166 100644 --- a/test/host/BaseHostPlugin.js +++ b/test/host/BaseHostPlugin.js @@ -1,8 +1,6 @@ -"use strict"; -const assert = require("assert").strict; -const mock = require("../mock"); +import * as mock from "../mock.js"; -const host = require("@clusterio/host"); +import * as host from "@clusterio/host"; describe("host/src/BaseHostPlugin", function() { describe("class BaseHostPlugin", function() { diff --git a/test/host/BaseInstancePlugin.js b/test/host/BaseInstancePlugin.js index 20e077a2..71b6d252 100644 --- a/test/host/BaseInstancePlugin.js +++ b/test/host/BaseInstancePlugin.js @@ -1,8 +1,7 @@ -"use strict"; -const assert = require("assert").strict; -const mock = require("../mock"); +import assert from "node:assert/strict"; +import * as mock from "../mock.js"; -const host = require("@clusterio/host"); +import * as host from "@clusterio/host"; describe("host/src/BaseInstancePlugin", function() { describe("class BaseInstancePlugin", function() { diff --git a/test/host/Instance.js b/test/host/Instance.js index 8dd66f49..52623700 100644 --- a/test/host/Instance.js +++ b/test/host/Instance.js @@ -1,11 +1,10 @@ -"use strict"; -const assert = require("assert").strict; -const path = require("path"); +import assert from "node:assert/strict"; +import path from "node:path"; -const lib = require("@clusterio/lib"); -const { PlayerStats, wait } = lib; -const Instance = require("@clusterio/host/dist/node/src/Instance").default; -const { MockConnector, MockServer } = require("../mock"); +import * as lib from "@clusterio/lib"; +import { PlayerStats, wait } from "@clusterio/lib"; +import Instance from "@clusterio/host/dist/node/src/Instance.js"; +import { MockConnector, MockServer } from "../mock.js"; const addr = lib.Address.fromShorthand; diff --git a/test/host/export.js b/test/host/export.js index c806c316..a9131584 100644 --- a/test/host/export.js +++ b/test/host/export.js @@ -1,9 +1,8 @@ -"use strict"; -const assert = require("assert").strict; -const path = require("path"); +import assert from "node:assert/strict"; +import path from "node:path"; -const { FactorioServer } = require("@clusterio/host/dist/node/src/server"); -const { _exportLocale } = require("@clusterio/host/dist/node/src/export"); +import { FactorioServer } from "@clusterio/host/dist/node/src/server.js"; +import { _exportLocale } from "@clusterio/host/dist/node/src/export.js"; describe("host/src/export", function() { diff --git a/test/host/patch.js b/test/host/patch.js index 3df1ca5e..1e11aaca 100644 --- a/test/host/patch.js +++ b/test/host/patch.js @@ -1,11 +1,10 @@ -"use strict"; -const assert = require("assert").strict; -const fs = require("node:fs/promises"); -const JSZip = require("jszip"); -const path = require("path"); +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import JSZip from "jszip"; +import path from "node:path"; -const lib = require("@clusterio/lib"); -const patch = require("@clusterio/host/dist/node/src/patch"); +import * as lib from "@clusterio/lib"; +import * as patch from "@clusterio/host/dist/node/src/patch.js"; describe("host/patch", function() { diff --git a/test/host/server.js b/test/host/server.js index a18e6058..60a0e681 100644 --- a/test/host/server.js +++ b/test/host/server.js @@ -1,14 +1,12 @@ -"use strict"; -const assert = require("assert").strict; -const events = require("events"); -const fs = require("node:fs/promises"); -const path = require("path"); +import assert from "node:assert/strict"; +import events from "node:events"; +import fs from "node:fs/promises"; +import path from "node:path"; -const hostServer = require("@clusterio/host/dist/node/src/server"); -const lib = require("@clusterio/lib"); -const { wait } = lib; -const { testLines } = require("../lib/factorio/lines"); -const { slowTest, externalTest } = require("../integration"); +import * as hostServer from "@clusterio/host/dist/node/src/server.js"; +import { Link, wait } from "@clusterio/lib"; +import { testLines } from "../lib/factorio/lines.js"; +import { slowTest, externalTest } from "../integration/index.js"; describe("host/server", function() { @@ -326,7 +324,7 @@ describe("host/server", function() { static jsonSchema = { type: "number" }; static fromJSON(json) { return new this(json); } } - let eventFromJSON = lib.Link.eventFromJSON(NumberEvent, "NumberEvent"); + let eventFromJSON = Link.eventFromJSON(NumberEvent, "NumberEvent"); ipcServer.handle("bad_event", async () => { eventFromJSON("not a number"); }); ipcServer.emit("ipc-bad_event", {}); await new Promise(resolve => setImmediate(resolve)); diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js index 26d8dfa9..7ef99463 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -1,22 +1,21 @@ -"use strict"; -const assert = require("assert").strict; -const fs = require("node:fs/promises"); -const jwt = require("jsonwebtoken"); -const path = require("path"); -const events = require("events"); +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import jwt from "jsonwebtoken"; +import path from "node:path"; +import events from "node:events"; -const lib = require("@clusterio/lib"); -const libBuildMod = require("@clusterio/lib/build_mod"); -const { wait } = lib; +import * as lib from "@clusterio/lib"; +import { wait } from "@clusterio/lib"; +import * as libBuildMod from "@clusterio/lib/build_mod.js"; -const testStrings = require("../lib/factorio/test_strings"); -const { +import * as testStrings from "../lib/factorio/test_strings.js"; +import { TestControl, TestControlConnector, url, controlToken, slowTest, execCtl, execCtlProcess, execController, execHost, sendRcon, getControl, spawnNode, instancesDir, factorioDir, databaseDir, controllerConfigPath, requiresFactorio, hasFactorio, -} = require("./index"); -const { ControllerEcho, HostEchoReceived } = require("../file/test_plugin/messages"); +} from "./index.js"; +import { ControllerEcho, HostEchoReceived } from "../file/test_plugin/messages.js"; /** @returns {Promise>} */ diff --git a/test/integration/create.js b/test/integration/create.js index 74784c11..1375fa29 100644 --- a/test/integration/create.js +++ b/test/integration/create.js @@ -1,11 +1,10 @@ -"use strict"; -const assert = require("assert").strict; -const fs = require("node:fs/promises"); -const jwt = require("jsonwebtoken"); -const path = require("path"); -const events = require("events"); -const child_process = require("child_process"); -const util = require("util"); +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import jwt from "jsonwebtoken"; +import path from "node:path"; +import events from "node:events"; +import child_process from "node:child_process"; +import util from "node:util"; const cwd = path.join("temp", "test_integration"); diff --git a/test/integration/export.js b/test/integration/export.js index 45169e09..6555801a 100644 --- a/test/integration/export.js +++ b/test/integration/export.js @@ -1,11 +1,10 @@ -"use strict"; -const assert = require("assert").strict; -const path = require("path"); +import assert from "node:assert/strict"; +import path from "node:path"; -const { _exportLocale } = require("@clusterio/host/dist/node/src/export"); -const { FactorioServer } = require("@clusterio/host/dist/node/src/server"); +import { _exportLocale } from "@clusterio/host/dist/node/src/export.js"; +import { FactorioServer } from "@clusterio/host/dist/node/src/server.js"; -const { factorioDir, requiresFactorio } = require("./index"); +import { factorioDir, requiresFactorio } from "./index.js"; describe("Integration of lib/factorio/export", function() { diff --git a/test/integration/index.js b/test/integration/index.js index 2b81631a..2c6910db 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -1,27 +1,26 @@ /* eslint-disable no-console */ -"use strict"; -const path = require("path"); -const fs = require("node:fs/promises"); -const { readFileSync } = require("node:fs"); -const child_process = require("child_process"); -const jwt = require("jsonwebtoken"); -const util = require("util"); -const events = require("events"); +import path from "node:path"; +import fs from "node:fs/promises"; +import { readFileSync } from "node:fs"; +import child_process from "node:child_process"; +import jwt from "jsonwebtoken"; +import util from "node:util"; +import events from "node:events"; -const lib = require("@clusterio/lib"); -const { LineSplitter, ConsoleTransport, logger } = lib; +import * as lib from "@clusterio/lib"; +import { LineSplitter, ConsoleTransport, logger } from "@clusterio/lib"; -const { selectTargetCommand, initialize: initializeCtl } = require("@clusterio/ctl"); -const { _listFactorioVersions } = require("@clusterio/host/dist/node/src/server"); +import { selectTargetCommand, initialize as initializeCtl } from "@clusterio/ctl"; +import { _listFactorioVersions } from "@clusterio/host/dist/node/src/server.js"; // Make sure permissions from plugins are loaded -require("../../plugins/global_chat/dist/node/index"); -require("../../plugins/player_auth/dist/node/index"); -require("../../plugins/research_sync/dist/node/index"); -require("../../plugins/statistics_exporter/dist/node/index"); -require("../../plugins/subspace_storage/dist/node/index"); +import "../../plugins/global_chat/dist/node/index.js"; +import "../../plugins/player_auth/dist/node/index.js"; +import "../../plugins/research_sync/dist/node/index.js"; +import "../../plugins/statistics_exporter/dist/node/index.js"; +import "../../plugins/subspace_storage/dist/node/index.js"; -class TestControl extends lib.Link { +export class TestControl extends lib.Link { constructor(connector, subscribe = true) { super(connector); this.hostUpdates = []; @@ -93,7 +92,7 @@ class TestControl extends lib.Link { } } -class TestControlConnector extends lib.WebSocketClientConnector { +export class TestControlConnector extends lib.WebSocketClientConnector { register() { this.sendHandshake( new lib.MessageRegisterControl( @@ -105,7 +104,7 @@ class TestControlConnector extends lib.WebSocketClientConnector { } } -class TestHostConnector extends lib.WebSocketClientConnector { +export class TestHostConnector extends lib.WebSocketClientConnector { register() { this.sendHandshake( new lib.MessageRegisterHost( @@ -119,7 +118,7 @@ class TestHostConnector extends lib.WebSocketClientConnector { } // Mark that this test takes a lot of time, or depends on a test that takes a lot of time. -function slowTest(test) { +export function slowTest(test) { if (process.env.FAST_TEST) { test.skip(); @@ -129,7 +128,7 @@ function slowTest(test) { } // Mark that this test depends on an external API and may be slow or flaky. -function externalTest(test) { +export function externalTest(test) { if (process.env.NO_EXTERNAL_TEST) { test.skip(); } @@ -138,7 +137,7 @@ function externalTest(test) { } // Mark that this test or suite of tests requires a factorio install to run. -function requiresFactorio(testOrSuite) { +export function requiresFactorio(testOrSuite) { if (testOrSuite.skip) { if (!haveFactorioInstall) { testOrSuite.skip(); @@ -152,11 +151,11 @@ function requiresFactorio(testOrSuite) { } } -function hasFactorio() { +export function hasFactorio() { return haveFactorioInstall; } -async function get(urlPath) { +export async function get(urlPath) { const url = new URL("https://localhost:4443"); url.pathname = urlPath; let res = await fetch(url); @@ -191,16 +190,16 @@ let haveFactorioInstall; const baseHostConfig = loadJSON("config-host.json"); -let url = "https://localhost:4443/"; -let controlToken = jwt.sign({ aud: "user", user: "test" }, Buffer.from("TestSecretDoNotUse", "base64")); -let instancesDir = path.join("temp", "test", "instances"); -let modsDir = path.join("temp", "test", "mods"); -let databaseDir = path.join("temp", "test", "database"); -let factorioDir = getFactorioDir(baseHostConfig); +export let url = "https://localhost:4443/"; +export let controlToken = jwt.sign({ aud: "user", user: "test" }, Buffer.from("TestSecretDoNotUse", "base64")); +export let instancesDir = path.join("temp", "test", "instances"); +export let modsDir = path.join("temp", "test", "mods"); +export let databaseDir = path.join("temp", "test", "database"); +export let factorioDir = getFactorioDir(baseHostConfig); let pluginListPath = path.join("temp", "test", "plugin-list.json"); -let controllerConfigPath = path.join("temp", "test", "config-controller.json"); -let hostConfigPath = path.join("temp", "test", "config-host.json"); -let controlConfigPath = path.join("temp", "test", "config-control.json"); +export let controllerConfigPath = path.join("temp", "test", "config-controller.json"); +export let hostConfigPath = path.join("temp", "test", "config-host.json"); +export let controlConfigPath = path.join("temp", "test", "config-control.json"); function childOptions(options) { return { @@ -210,29 +209,29 @@ function childOptions(options) { }; } -function exec(command, options = {}) { +export function exec(command, options = {}) { // Uncomment to show commands run in tests // console.log(command); return util.promisify(child_process.exec)(command, childOptions(options)); } -function execController(...args) { +export function execController(...args) { args[0] = `node --enable-source-maps ../../packages/controller ${args[0]}`; return exec(...args); } -function execHost(...args) { +export function execHost(...args) { args[0] = `node --enable-source-maps ../../packages/host ${args[0]}`; return exec(...args); } -function execCtlProcess(...args) { +export function execCtlProcess(...args) { args[0] = `node --enable-source-maps ../../packages/ctl ${args[0]}`; return exec(...args); } let inExecCtl = false; -async function execCtl(command) { +export async function execCtl(command) { inExecCtl = true; process.chdir("temp/test"); try { @@ -257,16 +256,16 @@ afterEach(function() { } }); -async function sendRcon(instanceId, command) { +export async function sendRcon(instanceId, command) { return await control.sendTo({ instanceId }, new lib.InstanceSendRconRequest(command)); } -function getControl() { +export function getControl() { return control; } /** @returns {Promise} */ -function spawn(name, cmd, waitFor, options = {}) { +export function spawn(name, cmd, waitFor, options = {}) { const silent = process.env.SILENT_TEST; const bootstrap = !controllerProcess || !hostProcess; @@ -303,7 +302,7 @@ function spawn(name, cmd, waitFor, options = {}) { }); } -function spawnNode(name, cmd, waitFor, options) { +export function spawnNode(name, cmd, waitFor, options) { return spawn(name, `node --enable-source-maps ${cmd}`, waitFor, options); } @@ -437,34 +436,3 @@ process.on("exit", () => { if (hostProcess) { hostProcess.kill(); } if (controllerProcess) { controllerProcess.kill(); } }); - - -module.exports = { - TestControl, - TestControlConnector, - TestHostConnector, - execController, - execCtlProcess, - slowTest, - externalTest, - requiresFactorio, - get, - exec, - execCtl, - execHost, - sendRcon, - getControl, - spawn, - spawnNode, - - url, - controlToken, - instancesDir, - modsDir, - databaseDir, - factorioDir, - hasFactorio, - controllerConfigPath, - hostConfigPath, - controlConfigPath, -}; diff --git a/test/integration/instance.js b/test/integration/instance.js index 56f150b7..8bcdec7d 100644 --- a/test/integration/instance.js +++ b/test/integration/instance.js @@ -1,14 +1,13 @@ -"use strict"; -const path = require("path"); -const assert = require("assert").strict; -const lib = require("@clusterio/lib"); -const fs = require("node:fs/promises"); +import path from "node:path"; +import assert from "node:assert/strict"; +import * as lib from "@clusterio/lib"; +import fs from "node:fs/promises"; -const { testMatrix } = require("../common"); -const { +import { testMatrix } from "../common.js"; +import { slowTest, exec, execCtl, execCtlProcess, sendRcon, getControl, requiresFactorio, hasFactorio, instancesDir, -} = require("./index"); +} from "./index.js"; const instId = 48; const instName = "Integration"; diff --git a/test/integration/link.js b/test/integration/link.js index 01483b86..d45a28ca 100644 --- a/test/integration/link.js +++ b/test/integration/link.js @@ -1,11 +1,9 @@ -"use strict"; -const assert = require("assert").strict; -const events = require("events"); -const fs = require("node:fs/promises"); -const jwt = require("jsonwebtoken"); +import assert from "node:assert/strict"; +import events from "node:events"; +import jwt from "jsonwebtoken"; -const { TestControlConnector, TestHostConnector, TestControl, get, exec, url, slowTest } = require("./index"); -const { ConnectionClosed, ProtocolError, PolicyViolation, AuthenticationFailed } = require("@clusterio/lib"); +import { TestControlConnector, TestHostConnector, TestControl, get, exec, url, slowTest } from "./index.js"; +import { ConnectionClosed, ProtocolError, PolicyViolation, AuthenticationFailed } from "@clusterio/lib"; let token = jwt.sign({ aud: "user", user: "test" }, Buffer.from("TestSecretDoNotUse", "base64")); let tokenHost = jwt.sign({ aud: "host", host: 0 }, Buffer.from("TestSecretDoNotUse", "base64")); diff --git a/test/integration/patch.js b/test/integration/patch.js index a4323e79..d222ff6d 100644 --- a/test/integration/patch.js +++ b/test/integration/patch.js @@ -1,17 +1,16 @@ -"use strict"; -const assert = require("assert").strict; -const fs = require("node:fs/promises"); -const JSZip = require("jszip"); -const path = require("path"); +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import JSZip from "jszip"; +import path from "node:path"; -const { patch, SaveModule } = require("@clusterio/host/dist/node/src/patch"); -const lib = require("@clusterio/lib"); +import { patch, SaveModule } from "@clusterio/host/dist/node/src/patch.js"; +import * as lib from "@clusterio/lib"; -const { slowTest } = require("./index"); +import { slowTest } from "./index.js"; // The server integration test is required to run before this one -require("./server"); +import "./server.js"; describe("Integration of lib/factorio/patch", function() { describe("patch()", function() { diff --git a/test/integration/routing.js b/test/integration/routing.js index 2f40745a..3aa2a9c9 100644 --- a/test/integration/routing.js +++ b/test/integration/routing.js @@ -1,12 +1,11 @@ -"use strict"; -const assert = require("assert").strict; +import assert from "node:assert/strict"; -const lib = require("@clusterio/lib"); -const { Host, Instance, InstanceConnection } = require("@clusterio/host"); -const { ControlConnection, Controller, HostConnection, InstanceRecord } = require("@clusterio/controller"); +import * as lib from "@clusterio/lib"; +import { Host, Instance, InstanceConnection } from "@clusterio/host"; +import { ControlConnection, Controller, HostConnection, InstanceRecord } from "@clusterio/controller"; // Although there are no imports, the file has side-effects such as "before" which can effect tests -require("./index"); +import "./index.js"; const addr = lib.Address.fromShorthand; diff --git a/test/integration/server.js b/test/integration/server.js index 8c8a711d..f0e08c30 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -1,15 +1,14 @@ -"use strict"; -const assert = require("assert").strict; -const events = require("events"); -const fs = require("node:fs/promises"); -const path = require("path"); +import assert from "node:assert/strict"; +import events from "node:events"; +import fs from "node:fs/promises"; +import path from "node:path"; -const lib = require("@clusterio/lib"); -const { InstanceManager } = require("@clusterio/controller"); -const { FactorioServer, _getFactorioVersion } = require("@clusterio/host/dist/node/src/server"); -const { logger } = lib; +import * as lib from "@clusterio/lib"; +import { InstanceManager } from "@clusterio/controller"; +import { FactorioServer, _getFactorioVersion } from "@clusterio/host/dist/node/src/server.js"; +import { logger } from "@clusterio/lib"; -const { slowTest, factorioDir, requiresFactorio } = require("./index"); +import { slowTest, factorioDir, requiresFactorio } from "./index.js"; describe("Integration of host/src/server", function() { diff --git a/test/lib/ExponentialBackoff.js b/test/lib/ExponentialBackoff.js index 5949deb2..431024ad 100644 --- a/test/lib/ExponentialBackoff.js +++ b/test/lib/ExponentialBackoff.js @@ -1,7 +1,6 @@ -"use strict"; -const assert = require("assert").strict; +import assert from "node:assert/strict"; -const { ExponentialBackoff } = require("@clusterio/lib"); +import { ExponentialBackoff } from "@clusterio/lib"; describe("lib/ExponentialBackoff", function() { describe("class ExponentialBackoff", function() { diff --git a/test/lib/LockFile.test.js b/test/lib/LockFile.test.js index f0903132..a4e25f4e 100644 --- a/test/lib/LockFile.test.js +++ b/test/lib/LockFile.test.js @@ -1,10 +1,9 @@ /* eslint-disable node/no-sync */ -"use strict"; -const assert = require("assert").strict; -const fs = require("node:fs/promises"); -const path = require("path"); +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import path from "node:path"; -const { LockFile } = require("@clusterio/lib"); +import { LockFile } from "@clusterio/lib"; const baseDir = path.join("temp", "test", "lock_file"); diff --git a/test/lib/ModStore.test.js b/test/lib/ModStore.test.js index b329d185..32e88e5e 100644 --- a/test/lib/ModStore.test.js +++ b/test/lib/ModStore.test.js @@ -1,15 +1,14 @@ -"use strict"; -const assert = require("assert").strict; -const path = require("path"); -const fs = require("node:fs/promises"); -const stream = require("node:stream"); -const JSZip = require("jszip"); // Added for creating mock zips +import assert from "node:assert/strict"; +import path from "node:path"; +import fs from "node:fs/promises"; +import stream from "node:stream"; +import JSZip from "jszip"; // Added for creating mock zips // Capture native fetch before any potential mocks are applied const nativeFetch = global.fetch; -const { ModStore, ModInfo, ModVersionEquality } = require("@clusterio/lib"); // Adjust path based on compiled output -const { externalTest } = require("../integration"); +import { ModStore, ModInfo, ModVersionEquality } from "@clusterio/lib"; // Adjust path based on compiled output +import { externalTest } from "../integration/index.js"; const MODS_DIR = path.join("temp", "test", "mod_store", "mods"); const CACHE_FILE = path.join(MODS_DIR, "mod-info-cache.json"); diff --git a/test/lib/RateLimiter.js b/test/lib/RateLimiter.js index f778d869..bc2611d0 100644 --- a/test/lib/RateLimiter.js +++ b/test/lib/RateLimiter.js @@ -1,7 +1,6 @@ -"use strict"; -const assert = require("assert").strict; +import assert from "node:assert/strict"; -const { RateLimiter, wait } = require("@clusterio/lib"); +import { RateLimiter, wait } from "@clusterio/lib"; describe("lib/RateLimiter", function() { diff --git a/test/lib/ValueCache.test.js b/test/lib/ValueCache.test.js index b75094c3..e0ee6890 100644 --- a/test/lib/ValueCache.test.js +++ b/test/lib/ValueCache.test.js @@ -1,5 +1,5 @@ -const assert = require("assert").strict; -const { ValueCache, wait } = require("@clusterio/lib"); +import assert from "node:assert/strict"; +import { ValueCache, wait } from "@clusterio/lib"; describe("ValueCache", function () { describe("constructor", function () { diff --git a/test/lib/check_singleton_import.test.js b/test/lib/check_singleton_import.test.js index c4af4938..4df9f2ba 100644 --- a/test/lib/check_singleton_import.test.js +++ b/test/lib/check_singleton_import.test.js @@ -1,12 +1,12 @@ -const assert = require("assert").strict; -const { checkSingletonImport } = require("@clusterio/lib/dist/node/src/check_singleton_import"); +import assert from "node:assert/strict"; +import { checkSingletonImport } from "@clusterio/lib/dist/node/src/check_singleton_import.js"; // Import lib so it sets the global indicating lib has been imported. -require("@clusterio/lib"); +import "@clusterio/lib"; describe("lib/check_singleton_import", function() { it("Should throw an installation error if called twice", function() { assert.throws( - () => checkSingletonImport(__filename), + () => checkSingletonImport(import.meta.filename), { message: /Attempt to import duplicate copy of @clusterio\/lib/, code: "InstallationError", diff --git a/test/lib/command.js b/test/lib/command.js index 86e299c6..f294a105 100644 --- a/test/lib/command.js +++ b/test/lib/command.js @@ -1,8 +1,7 @@ -"use strict"; -const assert = require("assert").strict; +import assert from "node:assert/strict"; -const lib = require("@clusterio/lib"); -const mock = require("../mock"); +import * as lib from "@clusterio/lib"; +import * as mock from "../mock.js"; const addr = lib.Address.fromShorthand; diff --git a/test/lib/config/classes.js b/test/lib/config/classes.js index 40e8cbb4..17ed8100 100644 --- a/test/lib/config/classes.js +++ b/test/lib/config/classes.js @@ -1,8 +1,7 @@ -"use strict"; -const lib = require("@clusterio/lib"); -const fs = require("node:fs/promises"); -const path = require("path"); -const assert = require("assert").strict; +import * as lib from "@clusterio/lib"; +import fs from "node:fs/promises"; +import path from "node:path"; +import assert from "node:assert/strict"; const CA = lib.ConfigAccess; diff --git a/test/lib/config/migrations.js b/test/lib/config/migrations.js index cb88c6d7..26dc6b03 100644 --- a/test/lib/config/migrations.js +++ b/test/lib/config/migrations.js @@ -1,6 +1,5 @@ -"use strict"; -const lib = require("@clusterio/lib"); -const assert = require("assert").strict; +import * as lib from "@clusterio/lib"; +import assert from "node:assert/strict"; // Each migration test should indicate which version introduced the migration diff --git a/test/lib/config/validators.test.js b/test/lib/config/validators.test.js index a8cdf907..be8b867d 100644 --- a/test/lib/config/validators.test.js +++ b/test/lib/config/validators.test.js @@ -1,8 +1,7 @@ -"use strict"; -const assert = require("assert").strict; -const lib = require("@clusterio/lib"); +import assert from "node:assert/strict"; +import * as lib from "@clusterio/lib"; const validators = lib.Config.validators; -const { ControllerConfig, HostConfig, InstanceConfig, ControlConfig } = lib; +import { ControllerConfig, HostConfig, InstanceConfig, ControlConfig } from "@clusterio/lib"; describe("lib/config/validators", function() { describe("all", function() { diff --git a/test/lib/data/HostDetails.test.js b/test/lib/data/HostDetails.test.js index 5c9405b3..3084718b 100644 --- a/test/lib/data/HostDetails.test.js +++ b/test/lib/data/HostDetails.test.js @@ -1,8 +1,7 @@ -"use strict"; -const assert = require("assert").strict; -const { HostDetails } = require("@clusterio/lib"); +import assert from "node:assert/strict"; +import { HostDetails } from "@clusterio/lib"; -const { testMatrix, testRoundTripJsonSerialisable } = require("../../common"); +import { testMatrix, testRoundTripJsonSerialisable } from "../../common.js"; describe("lib/data/HostDetails", function () { diff --git a/test/lib/data/InstanceDetails.test.js b/test/lib/data/InstanceDetails.test.js index cb949c18..95058653 100644 --- a/test/lib/data/InstanceDetails.test.js +++ b/test/lib/data/InstanceDetails.test.js @@ -1,8 +1,7 @@ -"use strict"; -const assert = require("assert").strict; -const { InstanceDetails } = require("@clusterio/lib"); +import assert from "node:assert/strict"; +import { InstanceDetails } from "@clusterio/lib"; -const { testMatrix, testRoundTripJsonSerialisable } = require("../../common"); +import { testMatrix, testRoundTripJsonSerialisable } from "../../common.js"; describe("lib/data/InstanceDetails", function () { diff --git a/test/lib/data/ModInfo.js b/test/lib/data/ModInfo.js index a90a5e4d..68926e4a 100644 --- a/test/lib/data/ModInfo.js +++ b/test/lib/data/ModInfo.js @@ -1,13 +1,12 @@ -"use strict"; -const assert = require("assert").strict; -const fs = require("node:fs/promises"); -const path = require("path"); +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import path from "node:path"; -const lib = require("@clusterio/lib"); -const libBuildMod = require("@clusterio/lib/build_mod"); -const { ModInfo, ModDependency } = lib; +import * as lib from "@clusterio/lib"; +import * as libBuildMod from "@clusterio/lib/build_mod.js"; +import { ModInfo, ModDependency } from "@clusterio/lib"; -const { testMatrix, testRoundTripJsonSerialisable } = require("../../common"); +import { testMatrix, testRoundTripJsonSerialisable } from "../../common.js"; describe("lib/data/ModInfo", function() { describe("class ModDependency", function() { diff --git a/test/lib/data/ModPack.js b/test/lib/data/ModPack.js index b813374b..ec21817a 100644 --- a/test/lib/data/ModPack.js +++ b/test/lib/data/ModPack.js @@ -1,9 +1,8 @@ -"use strict"; -const assert = require("assert").strict; -const zlib = require("zlib"); +import assert from "node:assert/strict"; +import zlib from "node:zlib"; -const lib = require("@clusterio/lib"); -const { ModInfo, ModPack, getInstalledModUpdates, applyModRecordAdvisories } = lib; +import * as lib from "@clusterio/lib"; +import { ModInfo, ModPack, getInstalledModUpdates, applyModRecordAdvisories } from "@clusterio/lib"; describe("lib/data/ModPack", function() { diff --git a/test/lib/data/Role.test.js b/test/lib/data/Role.test.js index c0f0d06c..bc908152 100644 --- a/test/lib/data/Role.test.js +++ b/test/lib/data/Role.test.js @@ -1,6 +1,5 @@ -"use strict"; -const assert = require("assert").strict; -const lib = require("@clusterio/lib"); +import assert from "node:assert/strict"; +import * as lib from "@clusterio/lib"; describe("lib/data/Role", function() { let defaultPermissions; diff --git a/test/lib/data/SystemInfo.js b/test/lib/data/SystemInfo.js index 7ee0b74f..d94cc406 100644 --- a/test/lib/data/SystemInfo.js +++ b/test/lib/data/SystemInfo.js @@ -1,8 +1,6 @@ -"use strict"; -const assert = require("assert").strict; +import assert from "node:assert/strict"; -const lib = require("@clusterio/lib"); -const { SystemInfo } = lib; +import { SystemInfo } from "@clusterio/lib"; describe("lib/data/SystemInfo", function() { diff --git a/test/lib/data/UserDetails.test.js b/test/lib/data/UserDetails.test.js index e155c4df..fa9b45ba 100644 --- a/test/lib/data/UserDetails.test.js +++ b/test/lib/data/UserDetails.test.js @@ -1,8 +1,7 @@ -"use strict"; -const assert = require("assert").strict; -const { UserDetails, PlayerStats } = require("@clusterio/lib"); +import assert from "node:assert/strict"; +import { UserDetails, PlayerStats } from "@clusterio/lib"; -const { testMatrix, testRoundTripJsonSerialisable } = require("../../common"); +import { testMatrix, testRoundTripJsonSerialisable } from "../../common.js"; describe("lib/data/UserDetails", function () { it("should be round trip serialisable", function () { diff --git a/test/lib/data/messages_core.js b/test/lib/data/messages_core.js index 2e06c673..461f913d 100644 --- a/test/lib/data/messages_core.js +++ b/test/lib/data/messages_core.js @@ -1,7 +1,5 @@ -"use strict"; -const assert = require("assert").strict; -const lib = require("@clusterio/lib"); -const { Address } = lib; +import assert from "node:assert/strict"; +import { Address } from "@clusterio/lib"; describe("lib/data/messages_core", function() { describe("class Address", function() { diff --git a/test/lib/data/version.test.js b/test/lib/data/version.test.js index 92bba3e9..ed43ca96 100644 --- a/test/lib/data/version.test.js +++ b/test/lib/data/version.test.js @@ -1,8 +1,7 @@ -"use strict"; -const assert = require("assert").strict; -const lib = require("@clusterio/lib"); +import assert from "node:assert/strict"; +import * as lib from "@clusterio/lib"; -const { testMatrix, testRoundTripJsonSerialisable } = require("../../common"); +import { testMatrix, testRoundTripJsonSerialisable } from "../../common.js"; describe("lib/data/version", function() { describe("isVersionEquality()", function() { diff --git a/test/lib/database.js b/test/lib/database.js index 4f01c942..b134eab5 100644 --- a/test/lib/database.js +++ b/test/lib/database.js @@ -1,9 +1,8 @@ -"use strict"; -const assert = require("assert").strict; -const fs = require("node:fs/promises"); -const path = require("path"); +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import path from "node:path"; -const lib = require("@clusterio/lib"); +import * as lib from "@clusterio/lib"; describe("lib/database", function() { describe("mapToObject()", function() { diff --git a/test/lib/datastore.js b/test/lib/datastore.js index 40a97ead..b4dc5cee 100644 --- a/test/lib/datastore.js +++ b/test/lib/datastore.js @@ -1,9 +1,8 @@ -"use strict"; -const events = require("events"); -const assert = require("assert").strict; -const lib = require("@clusterio/lib"); -const fs = require("node:fs/promises"); -const path = require("path"); +import events from "node:events"; +import assert from "node:assert/strict"; +import * as lib from "@clusterio/lib"; +import fs from "node:fs/promises"; +import path from "node:path"; class MockDatastoreProvider extends lib.DatastoreProvider { constructor() { diff --git a/test/lib/errors.js b/test/lib/errors.js index 5692d0a2..1cb4d296 100644 --- a/test/lib/errors.js +++ b/test/lib/errors.js @@ -1,7 +1,6 @@ -"use strict"; -const assert = require("assert").strict; +import assert from "node:assert/strict"; -const lib = require("@clusterio/lib"); +import * as lib from "@clusterio/lib"; describe("lib/errors", function() { describe("class InvalidMessage", function() { diff --git a/test/lib/external/FactorioVersions.test.js b/test/lib/external/FactorioVersions.test.js index b92d03a7..f63554d9 100644 --- a/test/lib/external/FactorioVersions.test.js +++ b/test/lib/external/FactorioVersions.test.js @@ -1,8 +1,8 @@ -const assert = require("assert").strict; -const { readFile } = require("node:fs/promises"); +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; -const { fetchFactorioVersions } = require("@clusterio/lib"); -const { slowTest, externalTest } = require("../../integration"); +import { fetchFactorioVersions } from "@clusterio/lib"; +import { slowTest, externalTest } from "../../integration/index.js"; describe("FactorioVersions", function() { describe("fetchFactorioVersions", function() { diff --git a/test/lib/external/LatestReleases.test.js b/test/lib/external/LatestReleases.test.js index 4fb79eff..686b6b52 100644 --- a/test/lib/external/LatestReleases.test.js +++ b/test/lib/external/LatestReleases.test.js @@ -1,7 +1,7 @@ -const assert = require("assert").strict; +import assert from "node:assert/strict"; -const { fetchLatestReleases, resolveReleaseChannel } = require("@clusterio/lib"); -const { slowTest, externalTest } = require("../../integration"); +import { fetchLatestReleases, resolveReleaseChannel } from "@clusterio/lib"; +import { slowTest, externalTest } from "../../integration/index.js"; describe("LatestReleases", function() { describe("fetchLatestReleases", function() { diff --git a/test/lib/factorio/exchange_string.js b/test/lib/factorio/exchange_string.js index eaea8458..9ea118dc 100644 --- a/test/lib/factorio/exchange_string.js +++ b/test/lib/factorio/exchange_string.js @@ -1,11 +1,10 @@ -"use strict"; -const assert = require("assert").strict; -const zlib = require("zlib"); +import assert from "node:assert/strict"; +import zlib from "node:zlib"; -const lib = require("@clusterio/lib"); +import * as lib from "@clusterio/lib"; -const testStrings = require("./test_strings"); -const testSettings = require("./test_settings"); +import * as testStrings from "./test_strings.js"; +import * as testSettings from "./test_settings.js"; // Simple linear congruential random number generator diff --git a/test/lib/factorio/lines.js b/test/lib/factorio/lines.js index f520c742..498150d6 100644 --- a/test/lib/factorio/lines.js +++ b/test/lib/factorio/lines.js @@ -1,7 +1,5 @@ // Lines for testing the factorio output - -"use strict"; -let testLines = new Map([ +export const testLines = new Map([ [ " 1.306 Info RemoteCommandProcessor.cpp:131: Starting RCON interface at port 4000", { @@ -219,7 +217,3 @@ let testLines = new Map([ }, ], ]); - -module.exports = { - testLines, -}; diff --git a/test/lib/factorio/test_settings.js b/test/lib/factorio/test_settings.js index fea7eb77..c1d397f4 100644 --- a/test/lib/factorio/test_settings.js +++ b/test/lib/factorio/test_settings.js @@ -1,6 +1,5 @@ -"use strict"; -const default_map_gen_settings = { +export const default_map_gen_settings = { autoplace_controls: { "coal": { frequency: 1, size: 1, richness: 1 }, "copper-ore": { frequency: 1, size: 1, richness: 1 }, @@ -37,7 +36,7 @@ const default_map_gen_settings = { water: 1, }; -const default_map_settings = { +export const default_map_settings = { pollution: { enabled: true, diffusion_ratio: 0.02, @@ -140,7 +139,7 @@ const default_map_settings = { }, }; -const modified_map_gen_settings = { +export const modified_map_gen_settings = { autoplace_controls: { "coal": { frequency: 1, size: 1, richness: 1 }, "copper-ore": { frequency: 0.5, size: 1, richness: 1 }, @@ -196,7 +195,7 @@ const modified_map_gen_settings = { water: 0.3333333432674408, }; -const modified_map_settings = { +export const modified_map_settings = { pollution: { enabled: true, diffusion_ratio: 0.02, @@ -299,7 +298,7 @@ const modified_map_settings = { }, }; -const modified_v2_map_gen_settings = { +export const modified_v2_map_gen_settings = { autoplace_controls: { "coal": { frequency: 1, size: 1, richness: 1 }, "copper-ore": { frequency: 0.5, size: 1, richness: 1 }, @@ -362,7 +361,7 @@ const modified_v2_map_gen_settings = { territory_settings: null, }; -const modified_v2_map_settings = { +export const modified_v2_map_settings = { pollution: { enabled: true, diffusion_ratio: 0.02, @@ -467,7 +466,7 @@ const modified_v2_map_settings = { }, }; -const modified_v2_1_map_gen_settings = { +export const modified_v2_1_map_gen_settings = { "autoplace_controls": { "coal": { "frequency": 1, "size": 1, "richness": 1 }, "copper-ore": { "frequency": 0.5, "size": 1, "richness": 1 }, @@ -516,7 +515,7 @@ const modified_v2_1_map_gen_settings = { "territory_settings": null, }; -const modified_v2_1_map_settings = { +export const modified_v2_1_map_settings = { "pollution": { "enabled": true, "diffusion_ratio": 0.02, @@ -615,7 +614,7 @@ const modified_v2_1_map_settings = { }, }; -const modified_v2_1_13_map_settings = { +export const modified_v2_1_13_map_settings = { ...modified_v2_1_map_settings, "enemy_expansion": { ...modified_v2_1_map_settings.enemy_expansion, @@ -623,7 +622,7 @@ const modified_v2_1_13_map_settings = { }, }; -const modified_space_age_map_gen_settings = { +export const modified_space_age_map_gen_settings = { autoplace_controls: { "aquilo_crude_oil": { frequency: 1, size: 1, richness: 1 }, "calcite": { frequency: 1, size: 1, richness: 1 }, @@ -701,16 +700,3 @@ const modified_space_age_map_gen_settings = { no_enemies_mode: false, territory_settings: null, }; - -module.exports = { - default_map_gen_settings, - default_map_settings, - modified_map_gen_settings, - modified_map_settings, - modified_v2_map_gen_settings, - modified_v2_map_settings, - modified_v2_1_map_gen_settings, - modified_v2_1_map_settings, - modified_v2_1_13_map_settings, - modified_space_age_map_gen_settings, -}; diff --git a/test/lib/factorio/test_strings.js b/test/lib/factorio/test_strings.js index eebad109..331993e9 100644 --- a/test/lib/factorio/test_strings.js +++ b/test/lib/factorio/test_strings.js @@ -1,7 +1,6 @@ -"use strict"; // Exchange strings for testing with -const default_ = ` +export default ` >>>eNpjZGBkUGQAgwZ7EOZgSc5PzIHxQJgrOb+gILVIN78oFVmYM 7moNCVVNz8TVXFqXmpupW5SYjGKYo7Movw8dBNYi0vy81BFSopSU 4uRRbhLixLzMktz0fUyMF5imubZ0CLHAML/6xkU/v8HYSDrAdAvI @@ -14,7 +13,7 @@ MPlQMQJEAG2EO4yRgjTod+B0UEeJiuJUALUb8SA7IYUhA9Pwqw9j GQ/mkMwIwLZH2giKg5YooELZGEKnHjBDHcNMDwvsMN4DvMdGJlBD JCqL0AxCA8kAzMKQgs4gIObmQEBgGmjrO/lHgDxa6G4<<<`; -const modified = ` +export const modified = ` >>>eNp1UzFs00AUvZ8mbRIEypCBSlCCyFAhOQqBKULxlQUxMCCxgnCc S2rh2On5LAgMZOjAgISEurQLXSmCBTGwRWKhEkgIJraiMgBiaGmFOiC FO5/PsUz6pfv+9/6/99+/kwEBOo2k6c83NhrZtOkaNkIDXa286fZ6hG @@ -31,7 +30,7 @@ jnh3gmXQRElVwYyxI8x4FMqOzsu4edrKK6hNZ5wU7V9G+ufEFL+7yHi cySQMp7wDHnRsBW571ORGn6fH2fUDj/FwV0iUbXPMbmTP7akkt8ChiL /BPceEs3j1M3iz71/ObT9tA==<<<`; -const modified_v2 = ` +export const modified_v2 = ` >>>eNp1Uz9oU0EY/y5N2iRWzRCFitSAGURIjNEpSN7VRRwcBFeJl5dL +ujLe+3de9XoYIYODoogXdrFrlZ0EQe3gIsFBdHJrVIHBYdWi3QQ4l3 u3csz1Q++7333+31/75IYAJwRCpNx0yU2QNfQmjbd+XnKCi6jgg/hlM @@ -49,7 +48,7 @@ Ln6IET6h2alhiMgvQ3SGxnDDDd32daT/yCD5fQ8R3WMEyeN/PENaNmy E5utYOI24z/cT+oQf48FdgozaFZg6qX+1KqW+GYyy4qNZgFNY/0IAHj RI5Q/27QvL<<<`; -const modified_v2_1 = ` +export const modified_v2_1 = ` >>>eNp1U89rE0EUftM0NYlVc4iCIjWHHkRICNFTkezoRTx4ELxKnGwm unSz087uVlsP5uDBg1CQXvSiVyt6EY9CwIu9if4DlXqwt1aK9CDEeZm dyZrgg/f49nu/37BTQCAPGYDZaVcwH6DnGC24YmmJy4qQHAAsnXdl3O @@ -65,7 +64,7 @@ JYkZplFakZG9jCkjt5gyQ4nHFv3qqTPmsjWiYeiVKOkP57RB9xB0Dvk X9skPmJ46b3GGNscC4FCtiwbc3PTPrUX46YL/qSDp8BMOqAJI8C+u/S peyl8e7GC3Ce2le3AIs019Y//gU3lOV1<<<`; -const modified_space_age = ` +export const modified_space_age = ` >>>eNp1VEFoE0EUnUkb27TWBomFSqkBexAhMVZPRbJbL+LBg+BV1slm kgzd7LYzs9HYgz304EEQpJf2Yq9W9CIevBW8WFAQPXmr1IOCh1aL9CD UmZ2dzW4aB2by972Z9//7MyQFALggJpjIogWfOJ5lU7+KLY84ACwZeg @@ -86,7 +85,7 @@ Gx0+5gy4RUpPimXd3JJg0hSVAZVaD42oXlGs+OdLeL8NIjXUO043NJp 38bydxUydeQi4j66kCmzxzUMyYTVaPneF1Uj+vlxQH+ZT82gl0Du2he Y+lJ/JEpK/WZNmBM/mgXgnKlfCACLzljjH+s/hYo=<<<`; -const modified_v2_1_13 = ` +export const modified_v2_1_13 = ` >>>eNp1U89rE0EUftMmNYm15hAFRWoOPYiQEKKgFMmOXsSDB0HwJHGy mejSzU47u1utHszBgwdBkF70olcrehHvAS/2JvUfqNSDvbVSpAehzsv sTNYEH7zHt9/7/YadAAIzSmE64wrmA/QcowVXLC5yWRGSA4Cl866M27 @@ -101,12 +100,3 @@ JYkZpmFakZGd1AkjtxhSQjYziizPKvHmmTPmUDWuYoiVKOgP57RB9yS 0Dvo1NMEfJJWw1i+YLmuywM1C1jwbPKaGnjffEMETl1yE9Qxu/9C7rp u3nVP+RQebGLpzeY4SxwbkUKGDDtjU/J9P3/nrIfNHXdPAWgFF7JHkZ 0L+YLmXPjcc3XoAz1D69BVjk1sULxb83nuUA<<<`; - -module.exports = { - default: default_, - modified, - modified_v2, - modified_v2_1, - modified_v2_1_13, - modified_space_age, -}; diff --git a/test/lib/file_ops.js b/test/lib/file_ops.js index 581cff94..036544a4 100644 --- a/test/lib/file_ops.js +++ b/test/lib/file_ops.js @@ -1,11 +1,10 @@ -"use strict"; -const assert = require("assert").strict; -const fs = require("node:fs/promises"); -const http = require("node:http"); -const path = require("path"); -const util = require("node:util"); +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import http from "node:http"; +import path from "node:path"; +import util from "node:util"; -const lib = require("@clusterio/lib"); +import * as lib from "@clusterio/lib"; describe("lib/file_ops", function() { let baseDir = path.join("temp", "test", "file_ops"); diff --git a/test/lib/hash.js b/test/lib/hash.js index 726c45eb..6ee9dc74 100644 --- a/test/lib/hash.js +++ b/test/lib/hash.js @@ -1,9 +1,8 @@ -"use strict"; -const assert = require("assert").strict; -const fs = require("node:fs/promises"); -const path = require("path"); +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import path from "node:path"; -const lib = require("@clusterio/lib"); +import * as lib from "@clusterio/lib"; describe("lib/hash", function() { diff --git a/test/lib/helpers.js b/test/lib/helpers.js index 27486882..2517ab6a 100644 --- a/test/lib/helpers.js +++ b/test/lib/helpers.js @@ -1,8 +1,7 @@ -"use strict"; -const assert = require("assert").strict; +import assert from "node:assert/strict"; const hrtime = process.hrtime.bigint; -const lib = require("@clusterio/lib"); +import * as lib from "@clusterio/lib"; describe("lib/helpers", function() { diff --git a/test/lib/ini.js b/test/lib/ini.js index b600df7c..7c1e57e0 100644 --- a/test/lib/ini.js +++ b/test/lib/ini.js @@ -1,7 +1,6 @@ -"use strict"; -const assert = require("assert").strict; -const fs = require("node:fs/promises"); -const lib = require("@clusterio/lib"); +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import * as lib from "@clusterio/lib"; describe("lib/ini", function() { describe("parse()", function() { diff --git a/test/lib/link/connector.js b/test/lib/link/connector.js index bbfbf39c..851b9e78 100644 --- a/test/lib/link/connector.js +++ b/test/lib/link/connector.js @@ -1,9 +1,8 @@ -"use strict"; -const assert = require("assert").strict; -const events = require("events"); +import assert from "node:assert/strict"; +import events from "node:events"; -const mock = require("../../mock"); -const lib = require("@clusterio/lib"); +import * as mock from "../../mock.js"; +import * as lib from "@clusterio/lib"; describe("lib/link/connectors", function() { diff --git a/test/lib/link/link.js b/test/lib/link/link.js index 112ae492..b6bec464 100644 --- a/test/lib/link/link.js +++ b/test/lib/link/link.js @@ -1,9 +1,8 @@ -"use strict"; -const assert = require("assert").strict; -const events = require("events"); +import assert from "node:assert/strict"; +import events from "node:events"; -const lib = require("@clusterio/lib"); -const mock = require("../../mock"); +import * as lib from "@clusterio/lib"; +import * as mock from "../../mock.js"; const addr = lib.Address.fromShorthand; diff --git a/test/lib/logging_utils.js b/test/lib/logging_utils.js index 919714e7..ba159555 100644 --- a/test/lib/logging_utils.js +++ b/test/lib/logging_utils.js @@ -1,10 +1,9 @@ -"use strict"; -const assert = require("assert").strict; -const fs = require("node:fs/promises"); -const path = require("path"); +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import path from "node:path"; -const { testLines } = require("./factorio/lines"); -const lib = require("@clusterio/lib"); +import { testLines } from "./factorio/lines.js"; +import * as lib from "@clusterio/lib"; describe("lib/logging_utils.js", function() { diff --git a/test/lib/lua_tools.js b/test/lib/lua_tools.js index 8b70b274..64c49e80 100644 --- a/test/lib/lua_tools.js +++ b/test/lib/lua_tools.js @@ -1,7 +1,6 @@ -"use strict"; -const assert = require("assert").strict; +import assert from "node:assert/strict"; -const lib = require("@clusterio/lib"); +import * as lib from "@clusterio/lib"; describe("lib/lua_tools", function() { diff --git a/test/lib/plugin.js b/test/lib/plugin.js index 30587b30..b9fd11ac 100644 --- a/test/lib/plugin.js +++ b/test/lib/plugin.js @@ -1,8 +1,7 @@ -"use strict"; -const assert = require("assert").strict; +import assert from "node:assert/strict"; -const mock = require("../mock"); -const lib = require("@clusterio/lib"); +import * as mock from "../mock.js"; +import * as lib from "@clusterio/lib"; describe("lib/plugin", function() { diff --git a/test/lib/plugin_loader.js b/test/lib/plugin_loader.js index 58281988..9d4b3ef2 100644 --- a/test/lib/plugin_loader.js +++ b/test/lib/plugin_loader.js @@ -1,10 +1,10 @@ -"use strict"; -const assert = require("assert").strict; -const fs = require("node:fs/promises"); -const path = require("path"); +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; -const lib = require("@clusterio/lib"); -const { BaseControllerPlugin } = require("@clusterio/controller"); +import * as lib from "@clusterio/lib"; +import { BaseControllerPlugin } from "@clusterio/controller"; describe("lib/plugin_loader", function() { @@ -19,7 +19,7 @@ describe("lib/plugin_loader", function() { await fs.mkdir(pluginPath, { recursive: true }); await fs.writeFile( path.join(pluginPath, "index.js"), - `module.exports.plugin = { name: "${infoName}" };` + `export const plugin = { name: "${infoName}" };` ); await fs.writeFile( path.join(pluginPath, "package.json"), @@ -56,7 +56,7 @@ describe("lib/plugin_loader", function() { it("should reject on broken plugin", async function() { let brokenMessage; try { - require(path.resolve(brokenPlugin)); + await import(pathToFileURL(path.resolve(path.join(brokenPlugin, "index.js")))); } catch (err) { brokenMessage = err.message; } @@ -84,7 +84,7 @@ describe("lib/plugin_loader", function() { await writeEntrypoint(missingClass, ""); await writeEntrypoint( - wrongParentClass, "class ControllerPlugin {}\n module.exports = { ControllerPlugin };\n" + wrongParentClass, "export class ControllerPlugin {};\n" ); }); it("should throw if class is missing from entrypoint", async function() { @@ -92,13 +92,13 @@ describe("lib/plugin_loader", function() { await assert.rejects( lib.loadPluginClass( "test", - path.posix.join(requirePath, "controller"), + path.posix.join(requirePath, "controller.js"), "ControllerPlugin", BaseControllerPlugin, ), { message: - `PluginError: Expected ${path.posix.join(requirePath, "controller")} ` + + `PluginError: Expected ${path.posix.join(requirePath, "controller.js")} ` + "to export a class named ControllerPlugin", } ); @@ -108,14 +108,14 @@ describe("lib/plugin_loader", function() { await assert.rejects( lib.loadPluginClass( "test", - path.posix.join(requirePath, "controller"), + path.posix.join(requirePath, "controller.js"), "ControllerPlugin", BaseControllerPlugin, ), { message: "PluginError: Expected ControllerPlugin exported from " + - `${path.posix.join(requirePath, "controller")} to be a subclass of BaseControllerPlugin`, + `${path.posix.join(requirePath, "controller.js")} to be a subclass of BaseControllerPlugin`, } ); }); @@ -152,7 +152,7 @@ describe("lib/plugin_loader", function() { await fs.mkdir(pluginPath, { recursive: true }); await fs.writeFile( path.join(pluginPath, "index.js"), - `module.exports.plugin = { name: "${name}" };` + `export const plugin = { name: "${name}" };` ); await fs.writeFile( path.join(pluginPath, "package.json"), @@ -195,7 +195,7 @@ describe("lib/plugin_loader", function() { await fs.mkdir(path.join(deepPluginPath, "dist")); await fs.writeFile( path.join(deepPluginPath, "dist", "index.js"), - 'module.exports.plugin = { name: "deep" };' + 'export const plugin = { name: "deep" };' ); // Create an npm module that is not a plugin diff --git a/test/lib/prometheus.js b/test/lib/prometheus.js index 8ef24147..0dcc4d93 100644 --- a/test/lib/prometheus.js +++ b/test/lib/prometheus.js @@ -1,7 +1,6 @@ -"use strict"; -const assert = require("assert").strict; +import assert from "node:assert/strict"; -const prometheus = require("@clusterio/lib"); +import * as prometheus from "@clusterio/lib"; describe("lib/prometheus", function() { diff --git a/test/lib/rce_ops.js b/test/lib/rce_ops.js index c5b272b7..dc3c56d3 100644 --- a/test/lib/rce_ops.js +++ b/test/lib/rce_ops.js @@ -1,6 +1,5 @@ -"use strict"; -const assert = require("assert").strict; -const lib = require("@clusterio/lib"); +import assert from "node:assert/strict"; +import * as lib from "@clusterio/lib"; describe("rce_ops", function() { describe("updatePackage", function() { diff --git a/test/lib/stream.js b/test/lib/stream.js index 144e7c06..09b8ee43 100644 --- a/test/lib/stream.js +++ b/test/lib/stream.js @@ -1,11 +1,10 @@ -"use strict"; -const assert = require("assert").strict; -const fs = require("node:fs/promises"); -const path = require("path"); -const stream = require("stream"); -const util = require("util"); +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import path from "node:path"; +import stream from "node:stream"; +import util from "node:util"; -const lib = require("@clusterio/lib"); +import * as lib from "@clusterio/lib"; const finished = util.promisify(stream.finished); diff --git a/test/lib/subscriptions.test.js b/test/lib/subscriptions.test.js index bd556487..97e7f943 100644 --- a/test/lib/subscriptions.test.js +++ b/test/lib/subscriptions.test.js @@ -1,9 +1,8 @@ -"use strict"; -const events = require("events"); -const assert = require("assert").strict; -const lib = require("@clusterio/lib"); -const { MockController, MockConnector, MockControl } = require("../mock"); -const { testMatrix, testRoundTripJsonSerialisable } = require("../common"); +import events from "node:events"; +import assert from "node:assert/strict"; +import * as lib from "@clusterio/lib"; +import { MockController, MockConnector, MockControl } from "../mock.js"; +import { testMatrix, testRoundTripJsonSerialisable } from "../common.js"; const addr = lib.Address.fromShorthand; diff --git a/test/lib/users.js b/test/lib/users.js index bb636191..344f304d 100644 --- a/test/lib/users.js +++ b/test/lib/users.js @@ -1,6 +1,5 @@ -"use strict"; -const assert = require("assert").strict; -const lib = require("@clusterio/lib"); +import assert from "node:assert/strict"; +import * as lib from "@clusterio/lib"; describe("lib/users", function() { diff --git a/test/lib/zip_ops.js b/test/lib/zip_ops.js index f9227021..99739d82 100644 --- a/test/lib/zip_ops.js +++ b/test/lib/zip_ops.js @@ -1,8 +1,7 @@ -"use strict"; -const assert = require("assert").strict; -const JSZip = require("jszip"); +import assert from "node:assert/strict"; +import JSZip from "jszip"; -const { findRoot } = require("@clusterio/lib"); +import { findRoot } from "@clusterio/lib"; describe("lib/factorio", function() { diff --git a/test/manual/test_settings_mod_pack.js b/test/manual/test_settings_mod_pack.js index fe2a699f..ae4e4110 100644 --- a/test/manual/test_settings_mod_pack.js +++ b/test/manual/test_settings_mod_pack.js @@ -1,5 +1,4 @@ -"use strict"; -const { ModPack } = require("@clusterio/lib"); +import { ModPack } from "@clusterio/lib"; const pack = new ModPack(); pack.name = "test-settings"; diff --git a/test/messages/controller.test.js b/test/messages/controller.test.js index de74d61c..d876a60c 100644 --- a/test/messages/controller.test.js +++ b/test/messages/controller.test.js @@ -1,12 +1,11 @@ -"use strict"; -const assert = require("assert").strict; -const fs = require("node:fs/promises"); -const os = require("node:os"); -const path = require("node:path"); -const lib = require("@clusterio/lib"); +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import * as lib from "@clusterio/lib"; -const { Controller, ControlConnection } = require("@clusterio/controller"); -const { testMatrix, testRoundTripJsonSerialisable } = require("../common"); +import { Controller, ControlConnection } from "@clusterio/controller"; +import { testMatrix, testRoundTripJsonSerialisable } from "../common.js"; describe("messages/controller", function() { /** @type {Controller} */ diff --git a/test/messages/host.test.js b/test/messages/host.test.js index 285d37c4..fe40256a 100644 --- a/test/messages/host.test.js +++ b/test/messages/host.test.js @@ -1,9 +1,8 @@ -"use strict"; -const assert = require("assert").strict; -const lib = require("@clusterio/lib"); +import assert from "node:assert/strict"; +import * as lib from "@clusterio/lib"; -const { Host } = require("@clusterio/host"); -const { testMatrix, testRoundTripJsonSerialisable } = require("../common"); +import { Host } from "@clusterio/host"; +import { testMatrix, testRoundTripJsonSerialisable } from "../common.js"; describe("messages/host", function() { /** @type {Host} */ diff --git a/test/messages/mod.test.js b/test/messages/mod.test.js index 09d56512..7a80b2eb 100644 --- a/test/messages/mod.test.js +++ b/test/messages/mod.test.js @@ -1,13 +1,12 @@ -"use strict"; -const assert = require("assert").strict; +import assert from "node:assert/strict"; -const lib = require("@clusterio/lib"); -const { ModDependencyResolveRequest, ModDependency, ModInfo } = lib; +import * as lib from "@clusterio/lib"; +import { ModDependencyResolveRequest, ModDependency, ModInfo } from "@clusterio/lib"; -const { testMatrix, testRoundTripJsonSerialisable } = require("../common"); +import { testMatrix, testRoundTripJsonSerialisable } from "../common.js"; -const { Controller, ControlConnection } = require("@clusterio/controller"); -const { externalTest } = require("../integration"); +import { Controller, ControlConnection } from "@clusterio/controller"; +import { externalTest } from "../integration/index.js"; describe("messages/mod", function() { /** @type {Controller} */ diff --git a/test/messages/plugin.test.js b/test/messages/plugin.test.js index e2c7d9d8..7935cd3c 100644 --- a/test/messages/plugin.test.js +++ b/test/messages/plugin.test.js @@ -1,9 +1,8 @@ -"use strict"; -const assert = require("assert").strict; -const lib = require("@clusterio/lib"); +import assert from "node:assert/strict"; +import * as lib from "@clusterio/lib"; -const { Controller, ControlConnection } = require("@clusterio/controller"); -const { Host } = require("@clusterio/host"); +import { Controller, ControlConnection } from "@clusterio/controller"; +import { Host } from "@clusterio/host"; describe("messages/plugin", function() { /** @type {Host} */ diff --git a/test/mock.js b/test/mock.js index 7cb9901f..1efdc3a5 100644 --- a/test/mock.js +++ b/test/mock.js @@ -1,14 +1,13 @@ -"use strict"; -const events = require("events"); -const http = require("http"); -const express = require("express"); +import events from "node:events"; +import http from "node:http"; +import express from "express"; -const lib = require("@clusterio/lib"); -const { User, UserManager, UserRecord, InstanceManager } = require("@clusterio/controller"); +import * as lib from "@clusterio/lib"; +import { User, UserManager, UserRecord, InstanceManager } from "@clusterio/controller"; const addr = lib.Address.fromShorthand; -class MockLogger { +export class MockLogger { child() { return this; } fatal() { } @@ -26,7 +25,7 @@ class MockLogger { verbose() { } } -class MockSocket { +export class MockSocket { constructor() { this.sentMessages = []; this.events = new Map(); @@ -66,7 +65,7 @@ class MockSocket { } } -class MockConnector extends lib.BaseConnector { +export class MockConnector extends lib.BaseConnector { constructor(src, dst) { super(src, dst); @@ -86,7 +85,7 @@ class MockConnector extends lib.BaseConnector { } } -class MockServer extends events.EventEmitter { +export class MockServer extends events.EventEmitter { constructor() { super(); this.reset(); @@ -116,7 +115,7 @@ class MockServer extends events.EventEmitter { } } -class MockInstance extends lib.Link { +export class MockInstance extends lib.Link { constructor() { super(new MockConnector(addr({ instanceId: 7357 }), addr({ hostId: 1 }))); this.logger = new MockLogger(); @@ -144,15 +143,15 @@ class MockInstance extends lib.Link { } } -class MockHost extends lib.Link { +export class MockHost extends lib.Link { constructor() { super(new MockConnector(addr({ hostId: 1 }), addr("controller"))); } } -class MockControl extends lib.Link { } +export class MockControl extends lib.Link { } -class MockController { +export class MockController { constructor() { this.app = express(); this.app.locals.controller = this; @@ -224,7 +223,7 @@ class MockController { } } -async function createControllerPlugin(ControllerPluginClass, info) { +export async function createControllerPlugin(ControllerPluginClass, info) { let controller = new MockController(); let metrics = {}; let logger = new MockLogger(); @@ -233,25 +232,10 @@ async function createControllerPlugin(ControllerPluginClass, info) { return plugin; } -async function createInstancePlugin(InstancePluginClass, info) { +export async function createInstancePlugin(InstancePluginClass, info) { let instance = new MockInstance(); let host = new MockHost(); let plugin = new InstancePluginClass(info, instance, host); await plugin.init(); return plugin; } - - -module.exports = { - MockLogger, - MockSocket, - MockConnector, - MockServer, - MockInstance, - MockHost, - MockControl, - MockController, - - createControllerPlugin, - createInstancePlugin, -}; From 68ce13ead408b020a93585070e9a2d68b57414b2 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:46:44 +0100 Subject: [PATCH 14/22] Fix failed not being cleared --- plugins/inventory_sync/module/serialize.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/inventory_sync/module/serialize.lua b/plugins/inventory_sync/module/serialize.lua index 7fabb07c..4aed3a93 100644 --- a/plugins/inventory_sync/module/serialize.lua +++ b/plugins/inventory_sync/module/serialize.lua @@ -557,6 +557,7 @@ function serialize.deserialize_crafting_notifications(player, serialized, previo --- @type CraftingNotificationDelta local delta = helpers.json_to_table(assert(helpers.decode_string(serialized))) assert(type(delta) == "table", "wrong type decoded from json_to_table") + local failed = {} for _, recipe_name in pairs(previous_failed or {}) do failed[recipe_name] = true @@ -567,10 +568,11 @@ function serialize.deserialize_crafting_notifications(player, serialized, previo for _, recipe in pairs(player.force.recipes) do if recipe.enabled and not recipe.hidden then enabled[recipe.name] = true + failed[recipe.name] = nil end end - -- Clear notifications the player has cleared elsewhere + -- Clear notifications the player has cleared elsewhere, this means "add seen notification" for _, recipe_name in pairs(delta.add or {}) do if enabled[recipe_name] then player.clear_recipe_notification(recipe_name) @@ -579,7 +581,7 @@ function serialize.deserialize_crafting_notifications(player, serialized, previo end end - -- Add notifications the player has elsewhere + -- Add notifications the player has elsewhere, this means "remove seen notification" for _, recipe_name in pairs(delta.remove or {}) do if enabled[recipe_name] then player.add_recipe_notification(recipe_name) From b1e79cf86dae5be62e8eb4934fa8c5670792fe08 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:20:56 +0000 Subject: [PATCH 15/22] Send only the local state on download init The current state sent with the download request no longer includes the failed names from the previous deserialization. Stored names unknown on this instance always end up in the add list, so the deserializer recomputes the failed list from that on every download and needs no previous state. Co-Authored-By: Claude Fable 5.1 --- .../inventory_sync/module/inventory_sync.lua | 9 ++---- plugins/inventory_sync/module/serialize.lua | 28 +++++-------------- 2 files changed, 9 insertions(+), 28 deletions(-) diff --git a/plugins/inventory_sync/module/inventory_sync.lua b/plugins/inventory_sync/module/inventory_sync.lua index 89250148..c6804251 100644 --- a/plugins/inventory_sync/module/inventory_sync.lua +++ b/plugins/inventory_sync/module/inventory_sync.lua @@ -112,9 +112,7 @@ function inventory_sync.deserialize_player(player, finished_record) -- Deserialize downloaded player data local serialized_player = compat.json_to_table(finished_record.data) assert(type(serialized_player) == "table", "wrong type for serialized_player") - script_data.failed_deserialization[player.name] = serialize.deserialize_player( - player, serialized_player, script_data.failed_deserialization[player.name] - ) + script_data.failed_deserialization[player.name] = serialize.deserialize_player(player, serialized_player) -- Restore player position and driving state restore_position(player, finished_record) @@ -447,10 +445,7 @@ function inventory_sync.initiate_inventory_download(player, player_record, gener -- The plugin only sends back what differs from the current notification state local recipe_notifications if recipe_notifications_api then - local failed = script_data.failed_deserialization[player.name] - recipe_notifications = serialize.serialize_crafting_notifications( - player, failed and failed.recipe_notifications - ) + recipe_notifications = serialize.serialize_crafting_notifications(player) end clusterio_api.send_json("inventory_sync_download", { diff --git a/plugins/inventory_sync/module/serialize.lua b/plugins/inventory_sync/module/serialize.lua index 4aed3a93..0ef6705e 100644 --- a/plugins/inventory_sync/module/serialize.lua +++ b/plugins/inventory_sync/module/serialize.lua @@ -551,33 +551,27 @@ end --- @param player LuaPlayer --- @param serialized string Encoded delta against the state sent with the download request ---- @param previous_failed string[]? Recipe names that did not exist at the last deserialization --- @return string[]? failed Recipe names that do not exist -function serialize.deserialize_crafting_notifications(player, serialized, previous_failed) +function serialize.deserialize_crafting_notifications(player, serialized) --- @type CraftingNotificationDelta local delta = helpers.json_to_table(assert(helpers.decode_string(serialized))) assert(type(delta) == "table", "wrong type decoded from json_to_table") - local failed = {} - for _, recipe_name in pairs(previous_failed or {}) do - failed[recipe_name] = true - end - -- Recipes which can have a notification on this server local enabled = {} for _, recipe in pairs(player.force.recipes) do if recipe.enabled and not recipe.hidden then enabled[recipe.name] = true - failed[recipe.name] = nil end end -- Clear notifications the player has cleared elsewhere, this means "add seen notification" + local failed = {} for _, recipe_name in pairs(delta.add or {}) do if enabled[recipe_name] then player.clear_recipe_notification(recipe_name) else - failed[recipe_name] = true -- Recipe does not exist on this server + failed[#failed + 1] = recipe_name -- Recipe does not exist on this server end end @@ -585,16 +579,10 @@ function serialize.deserialize_crafting_notifications(player, serialized, previo for _, recipe_name in pairs(delta.remove or {}) do if enabled[recipe_name] then player.add_recipe_notification(recipe_name) - else - failed[recipe_name] = nil end end - local failed_names = {} - for recipe_name in pairs(failed) do - failed_names[#failed_names + 1] = recipe_name - end - return next(failed_names) and failed_names or nil + return next(failed) and failed or nil end local controller_to_name = {} @@ -800,9 +788,8 @@ end --- @param player LuaPlayer --- @param serialized SerializedPlayerData ---- @param previous_failed FailedDeserializationPlayerData? --- @return FailedDeserializationPlayerData? -function serialize.deserialize_player(player, serialized, previous_failed) +function serialize.deserialize_player(player, serialized) local failed_deserialization = {} local target_controller = defines.controllers[serialized.controller] @@ -889,9 +876,8 @@ function serialize.deserialize_player(player, serialized, previous_failed) -- Deserialize recipe notifications if recipe_notifications_api and serialized.recipe_notifications then - failed_deserialization.recipe_notifications = serialize.deserialize_crafting_notifications( - player, serialized.recipe_notifications, previous_failed and previous_failed.recipe_notifications - ) + failed_deserialization.recipe_notifications = + serialize.deserialize_crafting_notifications(player, serialized.recipe_notifications) end return next(failed_deserialization) and failed_deserialization or nil From 8c03ac15abdbd39a6b2f46744a7573a24248ee62 Mon Sep 17 00:00:00 2001 From: Bastiaan <17990055+bbassie@users.noreply.github.com> Date: Sat, 12 Sep 2026 00:28:30 +0200 Subject: [PATCH 16/22] Define permissions on PluginDeclaration (#988) Adds a permissions array to PluginDeclaration, a type-level Permissions registry that plugins extend with module augmentation, and registerPluginPermissions() called from all entrypoints. checkPermission and the web UI hasPermission helpers now take PermissionName so typos are compile errors. Core definitions move into a corePermissions array that the interface derives its keys from. Closes #987 Co-authored-by: Claude Fable 5.1 Co-authored-by: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> --- docs/writing-plugins.md | 44 + packages/controller/controller.ts | 1 + packages/controller/src/ControlConnection.ts | 2 +- packages/controller/src/User.ts | 4 +- packages/create/templates/plugin-js/index.js | 69 +- .../create/templates/plugin-js/web/plugin.jsx | 2 +- packages/create/templates/plugin-ts/index.ts | 89 +- .../templates/plugin-ts/tsconfig.browser.json | 2 +- .../create/templates/plugin-ts/web/plugin.tsx | 2 +- packages/ctl/ctl.ts | 1 + packages/host/host.ts | 1 + packages/lib/src/data/UserDetails.ts | 4 +- packages/lib/src/permissions.ts | 1004 +++++++++-------- packages/lib/src/plugin.ts | 3 +- packages/lib/src/subscriptions.ts | 3 +- packages/web_ui/src/BaseWebPlugin.ts | 10 +- packages/web_ui/src/bootstrap.tsx | 1 + packages/web_ui/src/model/account.ts | 10 +- plugins/inventory_sync/index.ts | 18 +- plugins/inventory_sync/tsconfig.browser.json | 2 +- plugins/subspace_storage/index.ts | 18 +- .../subspace_storage/tsconfig.browser.json | 2 +- test/lib/users.js | 41 + 23 files changed, 758 insertions(+), 575 deletions(-) diff --git a/docs/writing-plugins.md b/docs/writing-plugins.md index f86e9582..84c4e983 100644 --- a/docs/writing-plugins.md +++ b/docs/writing-plugins.md @@ -10,6 +10,7 @@ The plugin classes have pre-defined hooks that are called during various stages - [Defining the plugin class](#defining-the-plugin-class) - [Logging Messages](#logging-messages) - [Plugin Configuration](#plugin-configuration) +- [Plugin Permissions](#plugin-permissions) - [Communicating with Factorio](#communicating-with-factorio) - [Defining Link Messages](#defining-link-messages) - [Sending Link Messages](#sending-link-messages) @@ -101,6 +102,10 @@ The following properties are recognized: Object with link messages definitions for this plugin. See guide for [defining link messages](#defining-link-messages) below. +**permissions**: + Array of permission definitions for this plugin. + See [Plugin Permissions](#plugin-permissions) + The optional module folder contains a Clusterio module that will be patched into the save when the plugin is loaded. See the section on [Clusterio Modules](developing-for-clusterio.md) in the Developing for Clusterio document. The only restriction imposed on modules embedded into plugins is that they must be named the same as the plugin. @@ -249,6 +254,45 @@ async init() { ``` +## Plugin Permissions + +Permissions are granted to roles and checked on users, both on the controller and in the web UI. +A plugin declares the permissions it uses under `permissions` in the `plugin` export. +Names must start with the plugin name followed by a dot. +For example in index.ts: + +```ts +import type * as lib from "@clusterio/lib"; + +declare module "@clusterio/lib" { + // Extend the interface of known permission names so that + // user.checkPermission(...) accepts our permission. + export interface Permissions { + "foo_frobber.frobnicate": never; + } +} + +export default { + ... + permissions: [ + { + name: "foo_frobber.frobnicate", + title: "Frobnicate", + description: "Run frobnication on instances.", + grantByDefault: false, // Whether the generated Player role is granted this permission + }, + ], +} satisfies lib.PluginDeclaration; +``` + +The `Permissions` interface only exists at the type level, its values are always `never` and only the keys are used. +Adding your permission names to it gives you autocomplete and a compile error on typos when calling `user.checkPermission`, `account.hasPermission` or setting the `permission` of a web UI page. +In JavaScript the `declare module` block is not needed. + +Permissions declared this way are registered when the plugin is loaded. +The older `lib.definePermission()` function still works, but a plugin should not use both for the same permission name. + + ## Communicating with Factorio For pushing data into Factorio there's RCON, which lets you send arbitrary Lua commands to invoke whatever code you want in the game. diff --git a/packages/controller/controller.ts b/packages/controller/controller.ts index 4c344ba9..2551aee0 100644 --- a/packages/controller/controller.ts +++ b/packages/controller/controller.ts @@ -402,6 +402,7 @@ async function initialize(): Promise { logger.info("Loading Plugin info"); const pluginInfos = await lib.loadPluginInfos(pluginList); lib.registerPluginMessages(pluginInfos); + lib.registerPluginPermissions(pluginInfos); lib.addPluginConfigFields(pluginInfos); const controllerConfigPath = args.config; diff --git a/packages/controller/src/ControlConnection.ts b/packages/controller/src/ControlConnection.ts index 63e2d667..cf110b7a 100644 --- a/packages/controller/src/ControlConnection.ts +++ b/packages/controller/src/ControlConnection.ts @@ -169,7 +169,7 @@ export default class ControlConnection extends BaseConnection { } if (typeof permission === "string") { - this.user.checkPermission(permission); + this.user.checkPermission(permission as lib.PermissionName); return; } diff --git a/packages/controller/src/User.ts b/packages/controller/src/User.ts index c4fe115f..c7d50147 100644 --- a/packages/controller/src/User.ts +++ b/packages/controller/src/User.ts @@ -1,7 +1,7 @@ import { IUser, Role, SubscribableDatastore, - PermissionError, permissions, + PermissionError, PermissionName, permissions, } from "@clusterio/lib"; import UserRecord from "./UserRecord.js"; @@ -126,7 +126,7 @@ export default class User extends UserRecord implements IUser { this.saveRecord(); } - checkPermission(permission: string) { + checkPermission(permission: PermissionName) { if (!permissions.has(permission)) { throw new Error(`permission ${permission} does not exist`); } diff --git a/packages/create/templates/plugin-js/index.js b/packages/create/templates/plugin-js/index.js index 932b460d..8ac2056f 100644 --- a/packages/create/templates/plugin-js/index.js +++ b/packages/create/templates/plugin-js/index.js @@ -1,38 +1,6 @@ import * as lib from "@clusterio/lib"; //%if multi_context import Messages from "./messages.js"; - -lib.definePermission({ - name: "__plugin_name__.example.permission.event", - title: "Example permission event", - description: "Example Description. Event. Change me in index.js", - grantByDefault: true, -}); - -lib.definePermission({ - name: "__plugin_name__.example.permission.request", - title: "Example permission request", - description: "Example Description. Request. Change me in index.js", - grantByDefault: true, -}); -//%endif -//%if controller & web // Subscribing requires web content and the controller - -lib.definePermission({ - name: "__plugin_name__.example.permission.subscribe", - title: "Example permission subscribe", - description: "Example Description. Subscribe. Change me in index.js", - grantByDefault: true, -}); -//%endif -//%if web - -lib.definePermission({ - name: "__plugin_name__.page.view", - title: "Example page view permission", - description: "Example Description. View. Change me in index.js", - grantByDefault: true, -}); //%endif export const plugin = { @@ -115,6 +83,43 @@ export const plugin = { //%if multi_context // Subscribing requires multi context ], //%endif +//%if multi_context | web + + permissions: [ +//%endif +//%if multi_context + { + name: "__plugin_name__.example.permission.event", + title: "Example permission event", + description: "Example Description. Event. Change me in index.js", + grantByDefault: true, + }, + { + name: "__plugin_name__.example.permission.request", + title: "Example permission request", + description: "Example Description. Request. Change me in index.js", + grantByDefault: true, + }, +//%endif +//%if controller & web // Subscribing requires web content and the controller + { + name: "__plugin_name__.example.permission.subscribe", + title: "Example permission subscribe", + description: "Example Description. Subscribe. Change me in index.js", + grantByDefault: true, + }, +//%endif +//%if web + { + name: "__plugin_name__.page.view", + title: "Example page view permission", + description: "Example Description. View. Change me in index.js", + grantByDefault: true, + }, +//%endif +//%if multi_context | web + ], +//%endif //%if web // Web content template has an example route which is the plugin name webEntrypoint: "./web", diff --git a/packages/create/templates/plugin-js/web/plugin.jsx b/packages/create/templates/plugin-js/web/plugin.jsx index 48f90cf9..69ac6364 100644 --- a/packages/create/templates/plugin-js/web/plugin.jsx +++ b/packages/create/templates/plugin-js/web/plugin.jsx @@ -57,7 +57,7 @@ export class WebPlugin extends BaseWebPlugin { permission: "__plugin_name__.example.permission.subscribe", //%endif //%if !controller - permission: "__plugin_name__.example.permission.view", + permission: "__plugin_name__.page.view", //%endif content: , }, diff --git a/packages/create/templates/plugin-ts/index.ts b/packages/create/templates/plugin-ts/index.ts index 8027808b..f35b29e4 100644 --- a/packages/create/templates/plugin-ts/index.ts +++ b/packages/create/templates/plugin-ts/index.ts @@ -1,40 +1,8 @@ import * as lib from "@clusterio/lib"; //%if multi_context import * as Messages from "./messages.js"; - -lib.definePermission({ - name: "__plugin_name__.example.permission.event", - title: "Example permission event", - description: "Example Description. Event. Change me in index.ts", - grantByDefault: true, -}); - -lib.definePermission({ - name: "__plugin_name__.example.permission.request", - title: "Example permission request", - description: "Example Description. Request. Change me in index.ts", - grantByDefault: true, -}); //%endif -//%if controller & web // Subscribing requires web content and the controller - -lib.definePermission({ - name: "__plugin_name__.example.permission.subscribe", - title: "Example permission subscribe", - description: "Example Description. Subscribe. Change me in index.ts", - grantByDefault: true, -}); -//%endif -//%if web - -lib.definePermission({ - name: "__plugin_name__.page.view", - title: "Example page view permission", - description: "Example Description. View. Change me in index.ts", - grantByDefault: true, -}); -//%endif -//%if config +//%if config | multi_context | web declare module "@clusterio/lib" { //%endif @@ -58,7 +26,23 @@ declare module "@clusterio/lib" { "__plugin_name__.myControlField": string; } //%endif -//%if config +//%if multi_context | web + export interface Permissions { +//%endif +//%if multi_context + "__plugin_name__.example.permission.event": never; + "__plugin_name__.example.permission.request": never; +//%endif +//%if controller & web // Subscribing requires web content and the controller + "__plugin_name__.example.permission.subscribe": never; +//%endif +//%if web + "__plugin_name__.page.view": never; +//%endif +//%if multi_context | web + } +//%endif +//%if config | multi_context | web } //%endif @@ -143,6 +127,43 @@ export const plugin: lib.PluginDeclaration = { //%if multi_context // Subscribing requires multi context ], //%endif +//%if multi_context | web + + permissions: [ +//%endif +//%if multi_context + { + name: "__plugin_name__.example.permission.event", + title: "Example permission event", + description: "Example Description. Event. Change me in index.ts", + grantByDefault: true, + }, + { + name: "__plugin_name__.example.permission.request", + title: "Example permission request", + description: "Example Description. Request. Change me in index.ts", + grantByDefault: true, + }, +//%endif +//%if controller & web // Subscribing requires web content and the controller + { + name: "__plugin_name__.example.permission.subscribe", + title: "Example permission subscribe", + description: "Example Description. Subscribe. Change me in index.ts", + grantByDefault: true, + }, +//%endif +//%if web + { + name: "__plugin_name__.page.view", + title: "Example page view permission", + description: "Example Description. View. Change me in index.ts", + grantByDefault: true, + }, +//%endif +//%if multi_context | web + ], +//%endif //%if web // Web content template has an example route which is the plugin name webEntrypoint: "./web", diff --git a/packages/create/templates/plugin-ts/tsconfig.browser.json b/packages/create/templates/plugin-ts/tsconfig.browser.json index fe5100ca..a8232b97 100644 --- a/packages/create/templates/plugin-ts/tsconfig.browser.json +++ b/packages/create/templates/plugin-ts/tsconfig.browser.json @@ -9,5 +9,5 @@ "noEmitOnError": true, "outDir": "${configDir}/dist/browser", }, - "include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ], + "include": [ "web/**/*.tsx", "web/**/*.ts", "index.ts", "messages.ts", "package.json" ], } diff --git a/packages/create/templates/plugin-ts/web/plugin.tsx b/packages/create/templates/plugin-ts/web/plugin.tsx index 3584a098..92fbb5cd 100644 --- a/packages/create/templates/plugin-ts/web/plugin.tsx +++ b/packages/create/templates/plugin-ts/web/plugin.tsx @@ -57,7 +57,7 @@ export class WebPlugin extends BaseWebPlugin { permission: "__plugin_name__.example.permission.subscribe", //%endif //%if !controller - permission: "__plugin_name__.example.permission.view", + permission: "__plugin_name__.page.view", //%endif content: , }, diff --git a/packages/ctl/ctl.ts b/packages/ctl/ctl.ts index b3141d5f..2625ea5a 100755 --- a/packages/ctl/ctl.ts +++ b/packages/ctl/ctl.ts @@ -108,6 +108,7 @@ export class Control extends lib.Link { async function loadPlugins(pluginList: Map) { let pluginInfos = await lib.loadPluginInfos(pluginList); lib.registerPluginMessages(pluginInfos); + lib.registerPluginPermissions(pluginInfos); lib.addPluginConfigFields(pluginInfos); let ctlPlugins = new Map(); diff --git a/packages/host/host.ts b/packages/host/host.ts index a1f9a9b8..37d7f14f 100755 --- a/packages/host/host.ts +++ b/packages/host/host.ts @@ -163,6 +163,7 @@ async function startHost() { logger.info("Loading Plugin info"); let pluginInfos = await lib.loadPluginInfos(pluginList); lib.registerPluginMessages(pluginInfos); + lib.registerPluginPermissions(pluginInfos); lib.addPluginConfigFields(pluginInfos); const hostConfigPath = args.config; diff --git a/packages/lib/src/data/UserDetails.ts b/packages/lib/src/data/UserDetails.ts index 28a1646a..ffdb2585 100644 --- a/packages/lib/src/data/UserDetails.ts +++ b/packages/lib/src/data/UserDetails.ts @@ -1,5 +1,5 @@ import { Static, Type } from "@sinclair/typebox"; - +import type { PermissionName } from "../permissions.js"; import PlayerStats from "./PlayerStats.js"; /** Represents a user in the cluster */ @@ -164,5 +164,5 @@ export interface IUser extends UserDetails { * @throws {Error} If the given permission does not exist. * @throws {libErrors.PermissionError} if the user does noh have the given permission. */ - checkPermission(permission: string): void + checkPermission(permission: PermissionName): void } diff --git a/packages/lib/src/permissions.ts b/packages/lib/src/permissions.ts index b7ef91a9..2c9abed0 100644 --- a/packages/lib/src/permissions.ts +++ b/packages/lib/src/permissions.ts @@ -6,10 +6,44 @@ * @author Hornwitser * @module lib/users */ +import type { PluginNodeEnvInfo, PluginWebpackEnvInfo } from "./plugin.js"; import { Permission } from "./data/index.js"; export const permissions = new Map(); +/** + * Registry of known permission names. + * + * Plugins add their permissions with module augmentation: + * + * ```ts + * declare module "@clusterio/lib" { + * export interface Permissions { + * "foo_frobber.frobnicate": never; + * } + * } + * ``` + * + * Only the keys matter, the values are always never. + */ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface Permissions extends Record<(typeof corePermissions)[number]["name"], never> {} + +/** Name of a permission known at compile time, see {@link Permissions}. */ +export type PermissionName = keyof Permissions; + +/** Definition of a permission, see {@link definePermission}. */ +export type PermissionDefinition = { + /** The internal name for the permission. */ + name: PermissionName, + /** User facing name for the permission. */ + title: string, + /** User facing description. */ + description: string, + /** If true this permission is granted by the generated Player role by default. */ + grantByDefault?: boolean, +}; + /** * Define a new user permission for Clusterio * @@ -17,25 +51,18 @@ export const permissions = new Map(); * granted to roles, and checked on users. Name should be `lower_case` * style and start with the plugin name followed by a dot. * - * @param def - The definition for the permission. - * @param def.name - The internal name for the permission. - * @param def.title - User facing name for permission. - * @param def.description - User facing description. - * @param def.grantByDefault - - * If true this role is granted by the generated Player role by default. + * Plugins should prefer declaring permissions in the `permissions` array + * of their {@link PluginDeclaration}, which is registered by + * {@link registerPluginPermissions}. * + * @param def - The definition for the permission. */ export function definePermission({ name, title, description, grantByDefault = false, -}: { - name: string, - title: string, - description: string, - grantByDefault?: boolean, -}) { +}: PermissionDefinition) { if (typeof name !== "string" || !name.length) { throw new Error("Expected name to be a non-empty string"); } @@ -56,477 +83,508 @@ export function definePermission({ permissions.set(name, new Permission(name, title, description, grantByDefault)); } +/** + * Define all permissions declared by the provided plugin infos + * + * @param pluginInfos - Array of plugin info objects. + */ +export function registerPluginPermissions(pluginInfos: PluginNodeEnvInfo[] | PluginWebpackEnvInfo[]) { + for (const pluginInfo of pluginInfos) { + for (const permission of pluginInfo.permissions ?? []) { + if (typeof permission.name !== "string" || !permission.name.startsWith(`${pluginInfo.name}.`)) { + throw new Error( + `Expected name of permission '${permission.name}' for ${pluginInfo.name} ` + + `to start with '${pluginInfo.name}.'` + ); + } + definePermission(permission); + } + } +} + // Definitions for the built in permissions used in Clusterio. // description should answer "this permission allows you to ___" -definePermission({ - name: "core.admin", - title: "Administrator", - description: "Bypass all permission checks.", -}); -definePermission({ - name: "core.control.connect", - title: "Connect to WebSocket", - description: "Connect to the controller's WebSocket control interface.", - grantByDefault: true, -}); +export const corePermissions = [ + { + name: "core.admin", + title: "Administrator", + description: "Bypass all permission checks.", + }, + { + name: "core.control.connect", + title: "Connect to WebSocket", + description: "Connect to the controller's WebSocket control interface.", + grantByDefault: true, + }, -definePermission({ - name: "core.controller.stop", - title: "Stop controller", - description: - "Stop the Node.js controller process making the cluster inoperable until someone with access to the system " + - "it runs on manually starts it again.", -}); -definePermission({ - name: "core.controller.restart", - title: "Restart controller", - description: "Restart the Node.js controller process if the system is set up for restarting.", -}); -definePermission({ - name: "core.controller.update", - title: "Update controller", - description: "Remotely update the controller if the controller allows for remote updates.", -}); -definePermission({ - name: "core.controller.get_config", - title: "Get controller config", - description: "Get the config of controller.", -}); -definePermission({ - name: "core.controller.update_config", - title: "Modify controller config", - description: "Modify the controller config or entries of the controller config.", -}); -definePermission({ - name: "core.system.subscribe", - title: "Subscribe to system updates", - description: - "Subscribe to be notified when the system info detailing system specific information along with metrics " + - "such as cpu, memory and disk usage for the controller and hosts are updated.", -}); -definePermission({ - name: "core.host.stop", - title: "Stop Hosts", - description: - "Stop Node.js host processes making the host inaccessible until someone with access to the system " + - "it runs on manually starts it again.", -}); -definePermission({ - name: "core.host.restart", - title: "Restart Hosts", - description: "Restart Node.js host processes if the system they run on are set up for restarting.", -}); -definePermission({ - name: "core.host.update", - title: "Update Hosts", - description: "Remotely update a host if the host allows for remote updates.", -}); -definePermission({ - name: "core.host.get_config", - title: "Get host configs", - description: "Get the config of hosts.", -}); -definePermission({ - name: "core.host.update_config", - title: "Modify host configs", - description: "Modify the host config or entries of the host config.", -}); -definePermission({ - name: "core.host.list", - title: "List hosts", - description: "Get the full list of hosts in the cluster.", - grantByDefault: true, -}); -definePermission({ - name: "core.host.subscribe", - title: "Subscribe to host updates", - description: "Subscribe to be notified on updates on the details of hosts.", - grantByDefault: true, -}); -definePermission({ - name: "core.host.generate_token", - title: "Generate host token", - description: "Generate tokens for hosts to connect to the cluster with.", -}); -definePermission({ - name: "core.host.revoke_token", - title: "Revoke host token", - description: "Revoke access token and terminate existing connection for hosts.", -}); -definePermission({ - name: "core.host.create_config", - title: "Create host config", - description: "Create host configs via the controller.", -}); + { + name: "core.controller.stop", + title: "Stop controller", + description: + "Stop the Node.js controller process making the cluster inoperable until someone with access to the " + + "system it runs on manually starts it again.", + }, + { + name: "core.controller.restart", + title: "Restart controller", + description: "Restart the Node.js controller process if the system is set up for restarting.", + }, + { + name: "core.controller.update", + title: "Update controller", + description: "Remotely update the controller if the controller allows for remote updates.", + }, + { + name: "core.controller.get_config", + title: "Get controller config", + description: "Get the config of controller.", + }, + { + name: "core.controller.update_config", + title: "Modify controller config", + description: "Modify the controller config or entries of the controller config.", + }, + { + name: "core.system.subscribe", + title: "Subscribe to system updates", + description: + "Subscribe to be notified when the system info detailing system specific information along with metrics " + + "such as cpu, memory and disk usage for the controller and hosts are updated.", + }, + { + name: "core.host.stop", + title: "Stop Hosts", + description: + "Stop Node.js host processes making the host inaccessible until someone with access to the system " + + "it runs on manually starts it again.", + }, + { + name: "core.host.restart", + title: "Restart Hosts", + description: "Restart Node.js host processes if the system they run on are set up for restarting.", + }, + { + name: "core.host.update", + title: "Update Hosts", + description: "Remotely update a host if the host allows for remote updates.", + }, + { + name: "core.host.get_config", + title: "Get host configs", + description: "Get the config of hosts.", + }, + { + name: "core.host.update_config", + title: "Modify host configs", + description: "Modify the host config or entries of the host config.", + }, + { + name: "core.host.list", + title: "List hosts", + description: "Get the full list of hosts in the cluster.", + grantByDefault: true, + }, + { + name: "core.host.subscribe", + title: "Subscribe to host updates", + description: "Subscribe to be notified on updates on the details of hosts.", + grantByDefault: true, + }, + { + name: "core.host.generate_token", + title: "Generate host token", + description: "Generate tokens for hosts to connect to the cluster with.", + }, + { + name: "core.host.revoke_token", + title: "Revoke host token", + description: "Revoke access token and terminate existing connection for hosts.", + }, + { + name: "core.host.create_config", + title: "Create host config", + description: "Create host configs via the controller.", + }, -definePermission({ - name: "core.instance.get", - title: "Get instance", - description: "Get the details of an instance in the cluster.", - grantByDefault: true, -}); -definePermission({ - name: "core.instance.list", - title: "List instances", - description: "Get the full list of instances in the cluster.", - grantByDefault: true, -}); -definePermission({ - name: "core.instance.subscribe", - title: "Subscribe to instance updates", - description: "Subscribe to be notified on updates on the details and status of instances.", - grantByDefault: true, -}); -definePermission({ - name: "core.instance.create", - title: "Create instance", - description: "Create new instances on the controller.", -}); -definePermission({ - name: "core.instance.get_config", - title: "Get instance config", - description: "Get the config of instances.", -}); -definePermission({ - name: "core.instance.update_config", - title: "Modify instance config", - description: "Modify the config or entries of the config for instances.", -}); -definePermission({ - name: "core.instance.assign", - title: "Assign instance", - description: "Assign or reassign instances to a host.", -}); -definePermission({ - name: "core.instance.save.list", - title: "List saves", - description: "List the saves currently on instances.", -}); -definePermission({ - name: "core.instance.save.subscribe", - title: "Subscribe to save updates", - description: "Subscribe to be notifed on updates to the list of saves of instances.", -}); -definePermission({ - name: "core.instance.save.create", - title: "Create new save", - description: "Create new savegames on instances.", -}); -definePermission({ - name: "core.instance.save.rename", - title: "Rename save", - description: "Rename savegames on instances.", -}); -definePermission({ - name: "core.instance.save.copy", - title: "Copy save", - description: "Create copies of savegames on instances.", -}); -definePermission({ - name: "core.instance.save.delete", - title: "Delete save", - description: "Delete savegames on instances.", -}); -definePermission({ - name: "core.instance.save.upload", - title: "Upload save", - description: "Upload savegames to instances.", -}); -definePermission({ - name: "core.instance.save.transfer", - title: "Transfer save", - description: "Transfer savegames between instances.", -}); -definePermission({ - name: "core.instance.save.download", - title: "Download save", - description: "Download savegames from instances.", -}); -definePermission({ - name: "core.instance.export_data", - title: "Export locale and icons", - description: "Export the the locale and icons from an instance and upload it to the controller.", -}); -definePermission({ - name: "core.instance.extract_players", - title: "Extract player stats from running save", - description: "Run extraction to create a user for each player in the save and set the online time from the save.", -}); -definePermission({ - name: "core.instance.start", - title: "Start instance", - description: "Start instances.", -}); -definePermission({ - name: "core.instance.restart", - title: "Restart instance", - description: "Restart instances.", -}); -definePermission({ - name: "core.instance.load_scenario", - title: "Load scenario", - description: "Start instances by loading a scenario.", -}); -definePermission({ - name: "core.instance.stop", - title: "Stop instance", - description: "Stop instances.", -}); -definePermission({ - name: "core.instance.kill", - title: "Kill instance", - description: "Terminate running instances without saving or cleanup.", -}); -definePermission({ - name: "core.instance.delete", - title: "Delete instance", - description: "Delete instances (includes config and all files stored like saves and logs).", -}); -definePermission({ - name: "core.instance.send_rcon", - title: "Send RCON", - description: "Send arbitrary RCON commands to instances.", -}); + { + name: "core.instance.get", + title: "Get instance", + description: "Get the details of an instance in the cluster.", + grantByDefault: true, + }, + { + name: "core.instance.list", + title: "List instances", + description: "Get the full list of instances in the cluster.", + grantByDefault: true, + }, + { + name: "core.instance.subscribe", + title: "Subscribe to instance updates", + description: "Subscribe to be notified on updates on the details and status of instances.", + grantByDefault: true, + }, + { + name: "core.instance.create", + title: "Create instance", + description: "Create new instances on the controller.", + }, + { + name: "core.instance.get_config", + title: "Get instance config", + description: "Get the config of instances.", + }, + { + name: "core.instance.update_config", + title: "Modify instance config", + description: "Modify the config or entries of the config for instances.", + }, + { + name: "core.instance.assign", + title: "Assign instance", + description: "Assign or reassign instances to a host.", + }, + { + name: "core.instance.save.list", + title: "List saves", + description: "List the saves currently on instances.", + }, + { + name: "core.instance.save.subscribe", + title: "Subscribe to save updates", + description: "Subscribe to be notifed on updates to the list of saves of instances.", + }, + { + name: "core.instance.save.create", + title: "Create new save", + description: "Create new savegames on instances.", + }, + { + name: "core.instance.save.rename", + title: "Rename save", + description: "Rename savegames on instances.", + }, + { + name: "core.instance.save.copy", + title: "Copy save", + description: "Create copies of savegames on instances.", + }, + { + name: "core.instance.save.delete", + title: "Delete save", + description: "Delete savegames on instances.", + }, + { + name: "core.instance.save.upload", + title: "Upload save", + description: "Upload savegames to instances.", + }, + { + name: "core.instance.save.transfer", + title: "Transfer save", + description: "Transfer savegames between instances.", + }, + { + name: "core.instance.save.download", + title: "Download save", + description: "Download savegames from instances.", + }, + { + name: "core.instance.export_data", + title: "Export locale and icons", + description: "Export the the locale and icons from an instance and upload it to the controller.", + }, + { + name: "core.instance.extract_players", + title: "Extract player stats from running save", + description: + "Run extraction to create a user for each player in the save and set the online time from the save.", + }, + { + name: "core.instance.start", + title: "Start instance", + description: "Start instances.", + }, + { + name: "core.instance.restart", + title: "Restart instance", + description: "Restart instances.", + }, + { + name: "core.instance.load_scenario", + title: "Load scenario", + description: "Start instances by loading a scenario.", + }, + { + name: "core.instance.stop", + title: "Stop instance", + description: "Stop instances.", + }, + { + name: "core.instance.kill", + title: "Kill instance", + description: "Terminate running instances without saving or cleanup.", + }, + { + name: "core.instance.delete", + title: "Delete instance", + description: "Delete instances (includes config and all files stored like saves and logs).", + }, + { + name: "core.instance.send_rcon", + title: "Send RCON", + description: "Send arbitrary RCON commands to instances.", + }, -definePermission({ - name: "core.mod_pack.get", - title: "Retrieve mod pack", - description: "Get the details of a mod pack in the cluster.", - grantByDefault: true, -}); -definePermission({ - name: "core.mod_pack.list", - title: "List mod packs", - description: "Get the full list of a mod packs in the cluster.", - grantByDefault: true, -}); -definePermission({ - name: "core.mod_pack.subscribe", - title: "Subscribe to mod pack updates", - description: "Subscribe to be notified on updates on the details of mod packs.", - grantByDefault: true, -}); -definePermission({ - name: "core.mod_pack.create", - title: "Create mod pack", - description: "Create new mod pack in the cluster.", -}); -definePermission({ - name: "core.mod_pack.update", - title: "Update mod pack", - description: "Edit existing mod pack definition in the cluster.", -}); -definePermission({ - name: "core.mod_pack.delete", - title: "Delete mod pack", - description: "Delete mod pack definition in the cluster.", -}); -definePermission({ - name: "core.mod.get", - title: "Get mods", - description: "Get the details of a mod stored in the cluster.", - grantByDefault: true, -}); -definePermission({ - name: "core.mod.list", - title: "List mods", - description: "Get the full list of mods stored on the controller.", - grantByDefault: true, -}); -definePermission({ - name: "core.mod.search", - title: "Search mods", - description: "Search through the list of mods stored on the controller.", - grantByDefault: true, -}); -definePermission({ - name: "core.mod.search_portal", - title: "Search mod portal", - description: "Search for mods on the Factorio mod portal.", - grantByDefault: true, -}); -definePermission({ - name: "core.mod.subscribe", - title: "Subscribe to mod updates", - description: "Subscribe to be notified on updates on the details of mods.", - grantByDefault: true, -}); -definePermission({ - name: "core.mod.upload", - title: "Upload mods", - description: "Upload mods to the controller.", -}); -definePermission({ - name: "core.mod.download", - title: "Download mod", - description: "Download mods stored on the controller.", - grantByDefault: true, -}); -definePermission({ - name: "core.mod.delete", - title: "Delete mod", - description: "Delete mods stored on the controller.", -}); -definePermission({ - name: "core.mod.download_from_portal", - title: "Download mods from portal", - description: "Allow downloading mods directly from the Factorio mod portal to the controller.", - grantByDefault: true, -}); + { + name: "core.mod_pack.get", + title: "Retrieve mod pack", + description: "Get the details of a mod pack in the cluster.", + grantByDefault: true, + }, + { + name: "core.mod_pack.list", + title: "List mod packs", + description: "Get the full list of a mod packs in the cluster.", + grantByDefault: true, + }, + { + name: "core.mod_pack.subscribe", + title: "Subscribe to mod pack updates", + description: "Subscribe to be notified on updates on the details of mod packs.", + grantByDefault: true, + }, + { + name: "core.mod_pack.create", + title: "Create mod pack", + description: "Create new mod pack in the cluster.", + }, + { + name: "core.mod_pack.update", + title: "Update mod pack", + description: "Edit existing mod pack definition in the cluster.", + }, + { + name: "core.mod_pack.delete", + title: "Delete mod pack", + description: "Delete mod pack definition in the cluster.", + }, + { + name: "core.mod.get", + title: "Get mods", + description: "Get the details of a mod stored in the cluster.", + grantByDefault: true, + }, + { + name: "core.mod.list", + title: "List mods", + description: "Get the full list of mods stored on the controller.", + grantByDefault: true, + }, + { + name: "core.mod.search", + title: "Search mods", + description: "Search through the list of mods stored on the controller.", + grantByDefault: true, + }, + { + name: "core.mod.search_portal", + title: "Search mod portal", + description: "Search for mods on the Factorio mod portal.", + grantByDefault: true, + }, + { + name: "core.mod.subscribe", + title: "Subscribe to mod updates", + description: "Subscribe to be notified on updates on the details of mods.", + grantByDefault: true, + }, + { + name: "core.mod.upload", + title: "Upload mods", + description: "Upload mods to the controller.", + }, + { + name: "core.mod.download", + title: "Download mod", + description: "Download mods stored on the controller.", + grantByDefault: true, + }, + { + name: "core.mod.delete", + title: "Delete mod", + description: "Delete mods stored on the controller.", + }, + { + name: "core.mod.download_from_portal", + title: "Download mods from portal", + description: "Allow downloading mods directly from the Factorio mod portal to the controller.", + grantByDefault: true, + }, -definePermission({ - name: "core.permission.list", - title: "List permissions", - description: "Get the full list of permissions in the cluster.", - grantByDefault: true, -}); + { + name: "core.permission.list", + title: "List permissions", + description: "Get the full list of permissions in the cluster.", + grantByDefault: true, + }, -definePermission({ - name: "core.role.list", - title: "List roles", - description: "Get the full list of roles and their permissions in the cluster.", - grantByDefault: true, -}); -definePermission({ - name: "core.role.subscribe", - title: "Subscribe to role updates", - description: "Subscribe to be notified on updates on the details of roles.", - grantByDefault: true, -}); -definePermission({ - name: "core.role.create", - title: "Create role", - description: "Create new roles with permissions.", -}); -definePermission({ - name: "core.role.update", - title: "Update role", - description: "Edit existing roles including permissions they grant.", -}); -definePermission({ - name: "core.role.delete", - title: "Delete role", - description: "Permanently delete roles.", -}); + { + name: "core.role.list", + title: "List roles", + description: "Get the full list of roles and their permissions in the cluster.", + grantByDefault: true, + }, + { + name: "core.role.subscribe", + title: "Subscribe to role updates", + description: "Subscribe to be notified on updates on the details of roles.", + grantByDefault: true, + }, + { + name: "core.role.create", + title: "Create role", + description: "Create new roles with permissions.", + }, + { + name: "core.role.update", + title: "Update role", + description: "Edit existing roles including permissions they grant.", + }, + { + name: "core.role.delete", + title: "Delete role", + description: "Permanently delete roles.", + }, -definePermission({ - name: "core.user.get", - title: "Get user", - description: "Get the details of a user in the cluster.", - grantByDefault: true, -}); -definePermission({ - name: "core.user.list", - title: "List users", - description: "Get the full list of users in the cluster.", - grantByDefault: true, -}); -definePermission({ - name: "core.user.subscribe", - title: "Subscribe to user updates", - description: "Subscribe to be notified on updates on the details and status of users.", - grantByDefault: true, -}); -definePermission({ - name: "core.user.create", - title: "Create users", - description: "Create user acounts with a given name.", -}); -definePermission({ - name: "core.user.revoke_token", - title: "Revoke user token", - description: "Revoke access token and terminate all existing control connections for oneself.", - grantByDefault: true, -}); -definePermission({ - name: "core.user.revoke_other_token", - title: "Revoke other user's token", - description: "Allow revoking tokens for other users than oneself. Requires Revoke user token.", -}); -definePermission({ - name: "core.user.update_roles", - title: "Update user roles", - description: "Add or remove any role to any user.", -}); -definePermission({ - name: "core.user.set_admin", - title: "Set user admin status", - description: "Promote or demote any user to Factorio admin.", -}); -definePermission({ - name: "core.user.set_banned", - title: "Set user ban status", - description: "Ban or unban any user.", -}); -definePermission({ - name: "core.user.set_whitelisted", - title: "Set user whitelist status", - description: "Add or remove any user to/from the whitelist.", -}); -definePermission({ - name: "core.user.delete", - title: "Delete user", - description: "Delete users and all data stored for them.", -}); -definePermission({ - name: "core.user.bulk_import", - title: "Bulk user import", - description: "Bulk import users including admin, whitelist, and bans." + - " (Imports types are restricted by other permissions, e.g. core.user.set_admin)", -}); -definePermission({ - name: "core.user.bulk_export", - title: "Bulk user export", - description: "Bulk export users including admin, whitelist, and bans.", -}); -definePermission({ - name: "core.user.bulk_restore", - title: "Bulk user restore", - description: "Bulk restore users including admin, whitelist, and bans." + - " (Restore types are restricted by other permissions, e.g. core.user.set_admin)", -}); + { + name: "core.user.get", + title: "Get user", + description: "Get the details of a user in the cluster.", + grantByDefault: true, + }, + { + name: "core.user.list", + title: "List users", + description: "Get the full list of users in the cluster.", + grantByDefault: true, + }, + { + name: "core.user.subscribe", + title: "Subscribe to user updates", + description: "Subscribe to be notified on updates on the details and status of users.", + grantByDefault: true, + }, + { + name: "core.user.create", + title: "Create users", + description: "Create user acounts with a given name.", + }, + { + name: "core.user.revoke_token", + title: "Revoke user token", + description: "Revoke access token and terminate all existing control connections for oneself.", + grantByDefault: true, + }, + { + name: "core.user.revoke_other_token", + title: "Revoke other user's token", + description: "Allow revoking tokens for other users than oneself. Requires Revoke user token.", + }, + { + name: "core.user.update_roles", + title: "Update user roles", + description: "Add or remove any role to any user.", + }, + { + name: "core.user.set_admin", + title: "Set user admin status", + description: "Promote or demote any user to Factorio admin.", + }, + { + name: "core.user.set_banned", + title: "Set user ban status", + description: "Ban or unban any user.", + }, + { + name: "core.user.set_whitelisted", + title: "Set user whitelist status", + description: "Add or remove any user to/from the whitelist.", + }, + { + name: "core.user.delete", + title: "Delete user", + description: "Delete users and all data stored for them.", + }, + { + name: "core.user.bulk_import", + title: "Bulk user import", + description: "Bulk import users including admin, whitelist, and bans." + + " (Imports types are restricted by other permissions, e.g. core.user.set_admin)", + }, + { + name: "core.user.bulk_export", + title: "Bulk user export", + description: "Bulk export users including admin, whitelist, and bans.", + }, + { + name: "core.user.bulk_restore", + title: "Bulk user restore", + description: "Bulk restore users including admin, whitelist, and bans." + + " (Restore types are restricted by other permissions, e.g. core.user.set_admin)", + }, -definePermission({ - name: "core.log.follow", - title: "Follow cluster log", - description: "Receive new entries in the cluster log. Required to see instance console.", -}); -definePermission({ - name: "core.log.query", - title: "Query cluster log", - description: "Query past entries in the cluster log. Required to see past entries in instance console.", -}); + { + name: "core.log.follow", + title: "Follow cluster log", + description: "Receive new entries in the cluster log. Required to see instance console.", + }, + { + name: "core.log.query", + title: "Query cluster log", + description: "Query past entries in the cluster log. Required to see past entries in instance console.", + }, -definePermission({ - name: "core.plugin.list", - title: "List plugins", - description: "List all installed plugins on a machine.", -}); -definePermission({ - name: "core.plugin.update", - title: "Update plugin", - description: "Remotely update a plugin if the target allows remote updates of plugins.", -}); -definePermission({ - name: "core.plugin.install", - title: "Install plugin", - description: "Remotely install a plugin if the target allows remote installs of plugins.", -}); + { + name: "core.plugin.list", + title: "List plugins", + description: "List all installed plugins on a machine.", + }, + { + name: "core.plugin.update", + title: "Update plugin", + description: "Remotely update a plugin if the target allows remote updates of plugins.", + }, + { + name: "core.plugin.install", + title: "Install plugin", + description: "Remotely install a plugin if the target allows remote installs of plugins.", + }, -definePermission({ - name: "core.debug.dump_ws", - title: "Dump WebSocket", - description: "Dump all WebSocket communicatation from the controller.", -}); + { + name: "core.debug.dump_ws", + title: "Dump WebSocket", + description: "Dump all WebSocket communicatation from the controller.", + }, -definePermission({ - name: "core.external.get_factorio_versions", - title: "Get factorio versions", - description: "Get list of all factorio versions from Wube", -}); + { + name: "core.external.get_factorio_versions", + title: "Get factorio versions", + description: "Get list of all factorio versions from Wube", + }, -definePermission({ - name: "core.external.get_latest_releases", - title: "Get factorio releases", - description: "Get the latest stable and experimental factorio releases from Wube", -}); + { + name: "core.external.get_latest_releases", + title: "Get factorio releases", + description: "Get the latest stable and experimental factorio releases from Wube", + }, +] as const satisfies readonly { + name: string, + title: string, + description: string, + grantByDefault?: boolean, +}[]; + +for (const permission of corePermissions) { + definePermission(permission); +} diff --git a/packages/lib/src/plugin.ts b/packages/lib/src/plugin.ts index b5060561..0f88f43a 100644 --- a/packages/lib/src/plugin.ts +++ b/packages/lib/src/plugin.ts @@ -5,9 +5,9 @@ import * as libHelpers from "./helpers.js"; import type { Logger } from "./logging.js"; import type { FieldDefinition } from "./config/index.js"; +import type { PermissionDefinition } from "./permissions.js"; import type { PlayerStats } from "./data/index.js"; - export const PluginFeatureFlags = [ /** The plugin requires module code to be patched into the save */ "SavePatching", @@ -37,6 +37,7 @@ export type PluginDeclaration = { features?: (typeof PluginFeatureFlags)[number][]; messages?: any[]; + permissions?: PermissionDefinition[]; routes?: string[]; } diff --git a/packages/lib/src/subscriptions.ts b/packages/lib/src/subscriptions.ts index 4324f53e..298bf5d7 100644 --- a/packages/lib/src/subscriptions.ts +++ b/packages/lib/src/subscriptions.ts @@ -2,6 +2,7 @@ import { Type, Static } from "@sinclair/typebox"; import { Link, Event, EventClass, RequestHandler, WebSocketBaseConnector } from "./link/index.js"; import { Address, MessageRequest, IUser, JsonBoolean, StringEnum } from "./data/index.js"; import isDeepStrictEqual from "#is_deep_strict_equal"; +import type { PermissionName } from "./permissions.js"; import { RequestError } from "./errors.js"; import { logger } from "./logging.js"; @@ -155,7 +156,7 @@ export class SubscriptionRequest { const entry = Link._eventsByName.get(data[0]); if (entry && entry.Event.permission) { if (typeof entry.Event.permission === "string") { - user.checkPermission(entry.Event.permission); + user.checkPermission(entry.Event.permission as PermissionName); } else { entry.Event.permission(user, message); } diff --git a/packages/web_ui/src/BaseWebPlugin.ts b/packages/web_ui/src/BaseWebPlugin.ts index 4ce654ae..35978615 100644 --- a/packages/web_ui/src/BaseWebPlugin.ts +++ b/packages/web_ui/src/BaseWebPlugin.ts @@ -1,5 +1,5 @@ import type React from "react"; -import type { AccountRole, FieldDefinition, Logger, PluginWebpackEnvInfo } from "@clusterio/lib"; +import type { AccountRole, FieldDefinition, Logger, PermissionName, PluginWebpackEnvInfo } from "@clusterio/lib"; import type { Control } from "./util/websocket"; /** @@ -37,11 +37,11 @@ export type UserAccount = { /** Roles of the corrently logged in account. */ roles: AccountRole[]; /** Check if the currently logged in account has the given permission. */ - hasPermission: (permission: string) => boolean | null; + hasPermission: (permission: PermissionName) => boolean | null; /** Check if the currently logged in account has any of the given permissions. */ - hasAnyPermission: (...permissions: string[]) => boolean | null; + hasAnyPermission: (...permissions: PermissionName[]) => boolean | null; /** Check if the currently logged in account has all of given permissions. */ - hasAllPermission: (...permissions: string[]) => boolean | null; + hasAllPermission: (...permissions: PermissionName[]) => boolean | null; /** Logs out of the web interface. */ logOut: () => void; }; @@ -75,7 +75,7 @@ export interface PluginPage { /** * Permission to access page. function are expected to throw an error if access is deny. */ - permission?: string | ((account: UserAccount) => (boolean|null)); + permission?: PermissionName | ((account: UserAccount) => (boolean|null)); }; /** diff --git a/packages/web_ui/src/bootstrap.tsx b/packages/web_ui/src/bootstrap.tsx index 2695d154..74359367 100644 --- a/packages/web_ui/src/bootstrap.tsx +++ b/packages/web_ui/src/bootstrap.tsx @@ -136,6 +136,7 @@ export default async function bootstrap() { })); let [pluginInfos, pluginSetKey] = await loadPluginInfos(); lib.registerPluginMessages(pluginInfos); + lib.registerPluginPermissions(pluginInfos); lib.addPluginConfigFields(pluginInfos); let wsUrl = new URL(webRoot, document.location.href); diff --git a/packages/web_ui/src/model/account.ts b/packages/web_ui/src/model/account.ts index dfce6832..fd18e5d1 100644 --- a/packages/web_ui/src/model/account.ts +++ b/packages/web_ui/src/model/account.ts @@ -22,13 +22,13 @@ export function useAccount(): UserAccount { }; }, [control]); - function checkPermissionExists(permission: string) { + function checkPermissionExists(permission: lib.PermissionName) { if (!lib.permissions.has(permission)) { throw new Error(`permission ${permission} does not exist`); } } - function hasPermission(permission: string): boolean { + function hasPermission(permission: lib.PermissionName): boolean { for (let role of roles) { if (role.permissions.includes("core.admin") || role.permissions.includes(permission)) { return true; @@ -40,14 +40,14 @@ export function useAccount(): UserAccount { return { name, roles, - hasPermission(permission: string): boolean | null { + hasPermission(permission: lib.PermissionName): boolean | null { if (!roles) { return null; } checkPermissionExists(permission); return hasPermission(permission); }, - hasAnyPermission(...permissions: string[]): boolean | null { + hasAnyPermission(...permissions: lib.PermissionName[]): boolean | null { if (!roles) { return null; } @@ -61,7 +61,7 @@ export function useAccount(): UserAccount { } return false; }, - hasAllPermission(...permissions: string[]): boolean | null { + hasAllPermission(...permissions: lib.PermissionName[]): boolean | null { if (!roles) { return null; } diff --git a/plugins/inventory_sync/index.ts b/plugins/inventory_sync/index.ts index dc78621b..17fddfc0 100644 --- a/plugins/inventory_sync/index.ts +++ b/plugins/inventory_sync/index.ts @@ -8,15 +8,11 @@ declare module "@clusterio/lib" { export interface ControllerConfigFields { "inventory_sync.player_lock_timeout": number; } + export interface Permissions { + "inventory_sync.inventory.view": never; + } } -lib.definePermission({ - name: "inventory_sync.inventory.view", - title: "View player inventories", - description: "View player inventories", - grantByDefault: true, -}); - export const plugin: lib.PluginDeclaration = { name: "inventory_sync", title: "Inventory sync", @@ -57,6 +53,14 @@ export const plugin: lib.PluginDeclaration = { messages.DownloadRequest, messages.DatabaseStatsRequest, ], + permissions: [ + { + name: "inventory_sync.inventory.view", + title: "View player inventories", + description: "View player inventories", + grantByDefault: true, + }, + ], webEntrypoint: "./web", routes: ["/inventory"], }; diff --git a/plugins/inventory_sync/tsconfig.browser.json b/plugins/inventory_sync/tsconfig.browser.json index 325fd71d..ba2d8c39 100644 --- a/plugins/inventory_sync/tsconfig.browser.json +++ b/plugins/inventory_sync/tsconfig.browser.json @@ -4,5 +4,5 @@ { "path": "../../packages/lib/tsconfig.browser.json" }, { "path": "../../packages/web_ui/tsconfig.browser.json" }, ], - "include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ], + "include": [ "web/**/*.tsx", "web/**/*.ts", "index.ts", "messages.ts", "package.json" ], } diff --git a/plugins/subspace_storage/index.ts b/plugins/subspace_storage/index.ts index 00cb5914..bdbecf03 100644 --- a/plugins/subspace_storage/index.ts +++ b/plugins/subspace_storage/index.ts @@ -9,15 +9,11 @@ declare module "@clusterio/lib" { export interface InstanceConfigFields { "subspace_storage.log_item_transfers": boolean; } + export interface Permissions { + "subspace_storage.storage.view": never; + } } -lib.definePermission({ - name: "subspace_storage.storage.view", - title: "View Subspace Storage", - description: "View the items and fluids stored in the shared subspace.", - grantByDefault: true, -}); - export const plugin: lib.PluginDeclaration = { name: "subspace_storage", title: "Subspace Storage", @@ -60,6 +56,14 @@ export const plugin: lib.PluginDeclaration = { messages.UpdateStorageEvent, messages.SetStorageSubscriptionRequest, ], + permissions: [ + { + name: "subspace_storage.storage.view", + title: "View Subspace Storage", + description: "View the items and fluids stored in the shared subspace.", + grantByDefault: true, + }, + ], webEntrypoint: "./web", routes: ["/storage"], }; diff --git a/plugins/subspace_storage/tsconfig.browser.json b/plugins/subspace_storage/tsconfig.browser.json index 325fd71d..ba2d8c39 100644 --- a/plugins/subspace_storage/tsconfig.browser.json +++ b/plugins/subspace_storage/tsconfig.browser.json @@ -4,5 +4,5 @@ { "path": "../../packages/lib/tsconfig.browser.json" }, { "path": "../../packages/web_ui/tsconfig.browser.json" }, ], - "include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ], + "include": [ "web/**/*.tsx", "web/**/*.ts", "index.ts", "messages.ts", "package.json" ], } diff --git a/test/lib/users.js b/test/lib/users.js index 344f304d..e8bd2479 100644 --- a/test/lib/users.js +++ b/test/lib/users.js @@ -43,4 +43,45 @@ describe("lib/users", function() { ); }); }); + + describe("registerPluginPermissions()", function() { + it("should define the permissions declared by plugins", function() { + lib.registerPluginPermissions([ + { name: "foo", permissions: [ + { name: "foo.first", title: "First", description: "The first" }, + { name: "foo.second", title: "Second", description: "The second", grantByDefault: true }, + ]}, + { name: "bar" }, + { name: "baz", permissions: [] }, + ]); + assert(lib.permissions.has("foo.first"), "foo.first was not defined"); + assert.equal(lib.permissions.get("foo.first").grantByDefault, false); + assert(lib.permissions.has("foo.second"), "foo.second was not defined"); + assert.equal(lib.permissions.get("foo.second").grantByDefault, true); + }); + + it("should throw if the name does not start with the plugin name", function() { + assert.throws( + () => lib.registerPluginPermissions([ + { name: "foo", permissions: [{ name: "bar.spam", title: "Spam", description: "Spam" }] }, + ]), + new Error("Expected name of permission 'bar.spam' for foo to start with 'foo.'") + ); + assert.throws( + () => lib.registerPluginPermissions([ + { name: "foo", permissions: [{ title: "Spam", description: "Spam" }] }, + ]), + new Error("Expected name of permission 'undefined' for foo to start with 'foo.'") + ); + }); + + it("should throw on already defined permission", function() { + assert.throws( + () => lib.registerPluginPermissions([ + { name: "foo", permissions: [{ name: "foo.first", title: "First", description: "The first" }] }, + ]), + new Error("Permission 'foo.first' is already defined") + ); + }); + }); }); From 8c20af64745f2ea720315789265f003ad40e0e4b Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:29:04 +0000 Subject: [PATCH 17/22] Convert inventory sync test to ES module Master migrated to ESM after this branch was created, so the CommonJS test failed to load on the merge commit. Co-Authored-By: Claude Fable 5.1 --- plugins/inventory_sync/test/instance.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/plugins/inventory_sync/test/instance.js b/plugins/inventory_sync/test/instance.js index 298a89ad..020f5979 100644 --- a/plugins/inventory_sync/test/instance.js +++ b/plugins/inventory_sync/test/instance.js @@ -1,14 +1,12 @@ -"use strict"; -const assert = require("assert").strict; -const util = require("util"); -const zlib = require("zlib"); +import assert from "node:assert/strict"; +import util from "node:util"; +import zlib from "node:zlib"; +import * as lib from "@clusterio/lib"; -const lib = require("@clusterio/lib"); - -const mock = require("../../../test/mock"); -const { recipeNotificationDelta, InstancePlugin } = require("../dist/node/instance"); -const { DownloadResponse } = require("../dist/node/messages"); -const info = require("../dist/node/index").plugin; +import * as mock from "../../../test/mock.js"; +import { recipeNotificationDelta, InstancePlugin } from "../dist/node/instance.js"; +import { DownloadResponse } from "../dist/node/messages.js"; +import { plugin as info } from "../dist/node/index.js"; const inflate = util.promisify(zlib.inflate); const deflate = util.promisify(zlib.deflate); From d4b6df4f636b7f8ce89f1faa9884d525ce75c1ab Mon Sep 17 00:00:00 2001 From: Bastiaan <17990055+bbassie@users.noreply.github.com> Date: Sat, 12 Sep 2026 02:49:14 +0200 Subject: [PATCH 18/22] Create config files with mode 0o600 (#1017) Config files hold the auth secret and tokens, so the default 0o644 lets every user on the system read them. Config.save, the ctl config written by the controller bootstrap, ctl host create-config and the config migration in create now pass mode 0o600. safeOutputFile also applies the mode to the temporary file it writes, since writeFile only honours mode on creation and a leftover temporary from a failed write would otherwise keep its old mode. Closes #1005 Co-authored-by: Claude Fable 5.1 --- packages/controller/controller.ts | 2 +- packages/create/create.js | 2 +- packages/ctl/src/commands_host.ts | 2 +- packages/lib/src/config/classes.ts | 2 +- packages/lib/src/file_ops.ts | 4 ++++ test/lib/config/classes.js | 9 +++++++++ test/lib/file_ops.js | 10 ++++++++++ 7 files changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/controller/controller.ts b/packages/controller/controller.ts index 2551aee0..a61a0080 100644 --- a/packages/controller/controller.ts +++ b/packages/controller/controller.ts @@ -206,7 +206,7 @@ async function handleBootstrapCommand( console.log(content); } else { logger.info(`Writing ${args.output}`); - await lib.safeOutputFile(args.output, content); + await lib.safeOutputFile(args.output, content, { mode: 0o600 }); } } } diff --git a/packages/create/create.js b/packages/create/create.js index 457dae2b..5980f9d2 100755 --- a/packages/create/create.js +++ b/packages/create/create.js @@ -234,7 +234,7 @@ async function migrateRename(args) { if (await pathExists(source) && !await pathExists(destination)) { await safeOutputFile(destination, JSON.stringify( renameConfig(JSON.parse(await fs.readFile(source))), null, "\t" - )); + ), { mode: 0o600 }); logger.info(`Migrated ${source} to ${destination}`); } } diff --git a/packages/ctl/src/commands_host.ts b/packages/ctl/src/commands_host.ts index fc269e3b..e2ad8283 100644 --- a/packages/ctl/src/commands_host.ts +++ b/packages/ctl/src/commands_host.ts @@ -87,7 +87,7 @@ hostCommands.add(new lib.Command({ } else { logger.info(`Writing ${args.output}`); try { - await fs.writeFile(args.output, content, { flag: "wx" }); + await fs.writeFile(args.output, content, { flag: "wx", mode: 0o600 }); } catch (err: any) { if (err.code === "EEXIST") { throw new lib.CommandError(`File ${args.output} already exists`); diff --git a/packages/lib/src/config/classes.ts b/packages/lib/src/config/classes.ts index 6ca54746..769079a0 100644 --- a/packages/lib/src/config/classes.ts +++ b/packages/lib/src/config/classes.ts @@ -468,7 +468,7 @@ export class Config< if (!this.filepath) { throw new Error("Cannot save config which has no filepath"); } - await safeOutputFile(this.filepath, JSON.stringify(this, null, "\t")); + await safeOutputFile(this.filepath, JSON.stringify(this, null, "\t"), { mode: 0o600 }); this.dirty = false; } diff --git a/packages/lib/src/file_ops.ts b/packages/lib/src/file_ops.ts index b8899406..908f57a3 100644 --- a/packages/lib/src/file_ops.ts +++ b/packages/lib/src/file_ops.ts @@ -168,6 +168,10 @@ export async function safeOutputFile( throw err; } } + if (options?.mode !== undefined) { + // writeFile only applies mode when creating, a leftover temporary keeps its old mode. + await fs.chmod(temporary, options.mode); + } await fs.rename(temporary, file); } diff --git a/test/lib/config/classes.js b/test/lib/config/classes.js index 17ed8100..c953ea13 100644 --- a/test/lib/config/classes.js +++ b/test/lib/config/classes.js @@ -395,6 +395,15 @@ describe("lib/config/classes", function() { const json = JSON.parse(await fs.readFile(filepath, "utf8")); assert.deepEqual(json, testInstance.toJSON()); }); + it("should not be readable by other users", async function() { + if (process.platform === "win32") { + this.skip(); + } + const testInstance = new TestConfig("local", { "alpha.foo": "a" }, filepath); + testInstance.set("alpha.foo", "b"); // Sets the dirty flag + await testInstance.save(); + assert.equal((await fs.stat(filepath)).mode & 0o777, 0o600); + }); it("should clear the dirty flag after saving", async function() { const testInstance = new TestConfig("local", { "alpha.foo": "a" }, filepath); testInstance.set("alpha.foo", "b"); // Sets the dirty flag diff --git a/test/lib/file_ops.js b/test/lib/file_ops.js index 036544a4..2f4993e7 100644 --- a/test/lib/file_ops.js +++ b/test/lib/file_ops.js @@ -100,6 +100,16 @@ describe("lib/file_ops", function() { await assert.rejects(fs.access(target.replace(".txt", ".tmp.txt")), "temporary was left behind"); assert.equal(await fs.readFile(target, "utf8"), "current"); }); + it("should apply mode to a leftover temporary file", async function() { + if (process.platform === "win32") { + this.skip(); + } + let target = path.join(baseDir, "safe", "mode.txt"); + await fs.writeFile(target.replace(".txt", ".tmp.txt"), "stale", { mode: 0o644 }); + await lib.safeOutputFile(target, "private", { mode: 0o600 }); + assert.equal((await fs.stat(target)).mode & 0o777, 0o600); + assert.equal(await fs.readFile(target, "utf8"), "private"); + }); it("should handle creating file in current working directory", async function() { let target = "temporary-file-made-to-test-cwd.txt"; try { From 5b0f629b6e1561bb14dc6523783072c5c583f1fb Mon Sep 17 00:00:00 2001 From: Bastiaan <17990055+bbassie@users.noreply.github.com> Date: Sat, 12 Sep 2026 02:51:39 +0200 Subject: [PATCH 19/22] Merge pull request #1016 from bbassie/fix/improve-logging Add context to connection and response log messages --- packages/controller/src/WsServerConnector.ts | 5 ++++- packages/lib/src/link/connectors.ts | 4 +++- packages/lib/src/link/link.ts | 4 ++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/controller/src/WsServerConnector.ts b/packages/controller/src/WsServerConnector.ts index 7277f8c3..439a2056 100644 --- a/packages/controller/src/WsServerConnector.ts +++ b/packages/controller/src/WsServerConnector.ts @@ -106,6 +106,9 @@ export default class WsServerConnector extends lib.WebSocketBaseConnector { this._state = "connected"; this._attachSocketHandlers(); this._dropSendBufferSeq(lastSeq); + logger.verbose( + `Connector | resuming session with ${this.dst}, resending ${this._sendBuffer.length} buffered messages` + ); for (let message of this._sendBuffer) { this._socket.send(JSON.stringify(message)); } @@ -113,7 +116,7 @@ export default class WsServerConnector extends lib.WebSocketBaseConnector { } _timedOut() { - logger.verbose("Connector | Connection timed out"); + logger.verbose(`Connector | Connection to ${this.dst} timed out`); this._close(); } diff --git a/packages/lib/src/link/connectors.ts b/packages/lib/src/link/connectors.ts index 8ce84f8f..d14b89e3 100644 --- a/packages/lib/src/link/connectors.ts +++ b/packages/lib/src/link/connectors.ts @@ -713,12 +713,14 @@ export abstract class WebSocketClientConnector extends WebSocketBaseConnector Date: Sat, 12 Sep 2026 02:53:45 +0200 Subject: [PATCH 20/22] Refuse mod packs for a newer Factorio than the instance runs (#1015) The mod pack's Factorio version ends up in mod-settings.dat. Factorio refuses to read that file when the version is newer than the game and falls back to default settings, which can wreck the save. Check the versions in syncMods before touching the mods folder. Closes #1006 Co-authored-by: Claude Fable 5.1 --- packages/host/src/Instance.ts | 20 ++++++++++++++++++++ test/host/Instance.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/packages/host/src/Instance.ts b/packages/host/src/Instance.ts index 755c7364..6e18661f 100644 --- a/packages/host/src/Instance.ts +++ b/packages/host/src/Instance.ts @@ -796,6 +796,7 @@ end`.replace(/\r?\n/g, " "); } else { modPack = await this.sendTo("controller", new lib.ModPackGetRequest(modPackId)); } + this.checkModPackVersion(modPack); this.activeModPack = modPack; const mods = await this._host.fetchMods(modPack.mods.values(), modPack.factorioVersion); @@ -847,6 +848,25 @@ end`.replace(/\r?\n/g, " "); await fs.writeFile(this.path("mods", "mod-settings.dat"), this.activeModPack.toModSettingsDat()); } + /** + * Check that the mod pack does not target a newer Factorio than the server + * + * The mod pack's Factorio version is written into mod-settings.dat and + * Factorio refuses to read it if it is newer than the game. It then + * loads default settings instead, which can corrupt the save. + * + * @throws {lib.RequestError} if the mod pack version is higher than the server version. + */ + checkModPackVersion(modPack: lib.ModPack) { + const serverVersion = this.server.version; + if (serverVersion && modPack.integerFactorioVersion > lib.integerFullVersion(serverVersion)) { + throw new lib.RequestError( + `Mod pack ${modPack.name} is for Factorio ${modPack.factorioVersion} ` + + `which is newer than the Factorio ${serverVersion} this instance runs` + ); + } + } + /** * Prepare instance for starting * diff --git a/test/host/Instance.js b/test/host/Instance.js index 52623700..73e11c1a 100644 --- a/test/host/Instance.js +++ b/test/host/Instance.js @@ -38,6 +38,34 @@ describe("class Instance", function() { }); }); + describe(".checkModPackVersion()", function() { + function modPack(factorioVersion) { + return lib.ModPack.fromJSON({ name: "pack", factorio_version: factorioVersion }); + } + + it("should accept a mod pack for an older or equal Factorio version", function() { + instance.server.version = "1.1.91"; + instance.checkModPackVersion(modPack("1.1")); + instance.checkModPackVersion(modPack("1.1.90")); + instance.checkModPackVersion(modPack("1.1.91")); + }); + + it("should reject a mod pack for a newer Factorio version", function() { + instance.server.version = "1.1.91"; + assert.throws( + () => instance.checkModPackVersion(modPack("1.1.110")), + new lib.RequestError( + "Mod pack pack is for Factorio 1.1.110 which is newer than the Factorio 1.1.91 this instance runs" + ) + ); + assert.throws(() => instance.checkModPackVersion(modPack("2.0")), lib.RequestError); + }); + + it("should skip the check when the server version is unknown", function() { + instance.checkModPackVersion(modPack("2.0")); + }); + }); + describe("._recordPlayerJoin()", function() { it("should add player to playersOnline", function() { instance._recordPlayerJoin("player"); From 1a10b38743e38d8fdb225e253da4da776491f989 Mon Sep 17 00:00:00 2001 From: Bastiaan <17990055+bbassie@users.noreply.github.com> Date: Sat, 12 Sep 2026 02:59:16 +0200 Subject: [PATCH 21/22] Only report a missing plugin web build when one is expected (#1013) Plugins with only host, instance or ctl code have no reason to ship a webpack build, yet the plugin list showed "Missing dist/web/manifest.json" as an error for them and the controller logged a warning on every start. Decide whether a build is expected from the plugin declaration, using the same rule the create tool uses to decide when to generate one: a web or controller entrypoint, or any config fields. Plugins outside that rule get no bundle path and no error, the web UI skips loading them, and the plugin pages show them as having no web module instead of an error or a prompt to reload. Fixes #1004 Co-authored-by: Claude Fable 5.1 --- packages/controller/src/Controller.ts | 4 +++- packages/controller/src/routes.ts | 2 +- packages/lib/src/api.ts | 3 ++- packages/lib/src/plugin.ts | 17 ++++++++++++++++ packages/web_ui/src/bootstrap.tsx | 3 +++ .../web_ui/src/components/PluginViewPage.tsx | 5 ++++- .../web_ui/src/components/PluginsPage.tsx | 13 +++++++++++- test/controller/routes.js | 15 +++++++++++++- test/lib/plugin.js | 20 +++++++++++++++++++ 9 files changed, 76 insertions(+), 6 deletions(-) diff --git a/packages/controller/src/Controller.ts b/packages/controller/src/Controller.ts index 7c7aa18b..48388665 100644 --- a/packages/controller/src/Controller.ts +++ b/packages/controller/src/Controller.ts @@ -1133,7 +1133,9 @@ export default class Controller { let manifestPath = path.posix.join(pluginInfo.requirePath, "dist", "web", "manifest.json"); pluginInfo.manifest = await Controller.loadJsonObject(import.meta.resolve(manifestPath), true); } catch (err) { - logger.warn(`Unable to load dist/web/manifest.json for plugin ${pluginInfo.name}`); + if (lib.pluginNeedsWebBuild(pluginInfo)) { + logger.warn(`Unable to load dist/web/manifest.json for plugin ${pluginInfo.name}`); + } } if (!this.config.get(`${pluginInfo.name}.load_plugin`)) { diff --git a/packages/controller/src/routes.ts b/packages/controller/src/routes.ts index 5e522583..bb8704fe 100644 --- a/packages/controller/src/routes.ts +++ b/packages/controller/src/routes.ts @@ -132,7 +132,7 @@ function getPlugins(req: Request, res: Response) { if (!web.main) { web.error = `Missing ${pluginInfo.name}.js entry in manifest.json`; } - } else { + } else if (lib.pluginNeedsWebBuild(pluginInfo)) { web.error = "Missing dist/web/manifest.json"; } if (web.main === "remoteEntry.js") { diff --git a/packages/lib/src/api.ts b/packages/lib/src/api.ts index 43289178..86a25393 100644 --- a/packages/lib/src/api.ts +++ b/packages/lib/src/api.ts @@ -4,7 +4,8 @@ export type PluginWebApi = { enabled: boolean; loaded: boolean; web: { - main: string; + /** Path to the web bundle, not present if the plugin has no web build. */ + main?: string; error?: string; }; /** diff --git a/packages/lib/src/plugin.ts b/packages/lib/src/plugin.ts index 0f88f43a..ae8e75c4 100644 --- a/packages/lib/src/plugin.ts +++ b/packages/lib/src/plugin.ts @@ -41,6 +41,23 @@ export type PluginDeclaration = { routes?: string[]; } +/** + * Check if a plugin is expected to ship a web build. + * + * Mirrors the rule used by the create tool: a web build is generated when the + * plugin has a web or controller entrypoint or defines config fields. + */ +export function pluginNeedsWebBuild(info: PluginDeclaration) { + return Boolean( + info.webEntrypoint + || info.controllerEntrypoint + || info.controllerConfigFields + || info.hostConfigFields + || info.instanceConfigFields + || info.controlConfigFields + ); +} + export type PluginNodeEnvInfo = PluginDeclaration & { /** * Path to the folder with the static files that should be hosted on the web diff --git a/packages/web_ui/src/bootstrap.tsx b/packages/web_ui/src/bootstrap.tsx index 74359367..1e4848b4 100644 --- a/packages/web_ui/src/bootstrap.tsx +++ b/packages/web_ui/src/bootstrap.tsx @@ -49,6 +49,9 @@ async function loadPluginInfos(): Promise<[lib.PluginWebpackEnvInfo[], string]> logger.error(`Failed to load plugin ${meta.name}: ${meta.web.error}`); continue; } + if (!meta.web.main) { + continue; + } try { await loadScript(`${staticRoot}${meta.web.main}`); let container: any = (window as { [key: string]: any })[`plugin_${meta.name}`]; diff --git a/packages/web_ui/src/components/PluginViewPage.tsx b/packages/web_ui/src/components/PluginViewPage.tsx index 58eba31a..5134b09c 100644 --- a/packages/web_ui/src/components/PluginViewPage.tsx +++ b/packages/web_ui/src/components/PluginViewPage.tsx @@ -46,11 +46,14 @@ export default function PluginViewPage() { } const pluginError = pluginMeta.web.error ?? pluginInfo?.error; + const hasWebModule = Boolean(pluginMeta.web.main || pluginMeta.web.error); if (!plugin) { return {pluginMeta.version} - No + + {hasWebModule ? "No" : "No web module"} + {pluginMeta.loaded ? "Yes" : "No"} {pluginMeta.enabled ? "Yes" : "No"} diff --git a/packages/web_ui/src/components/PluginsPage.tsx b/packages/web_ui/src/components/PluginsPage.tsx index 16a0dc7d..20d5ef84 100644 --- a/packages/web_ui/src/components/PluginsPage.tsx +++ b/packages/web_ui/src/components/PluginsPage.tsx @@ -88,6 +88,9 @@ export default function PluginsPage() { if (!plugin.meta.enabled) { return <> Disabled on controller; } + if (!plugin.meta.web.main && !plugin.meta.web.error) { + return plugin.meta.version; + } if (!plugin.meta.web.error && !control.pluginInfos.has(plugin.meta.name)) { return <> Reload page to load; } @@ -105,7 +108,15 @@ export default function PluginsPage() { { title: "Loaded", key: "loaded", - render: (_, plugin) => (plugin.package ? "Yes" : null), + render: (_, plugin) => { + if (plugin.package) { + return "Yes"; + } + if (!plugin.meta.web.main && !plugin.meta.web.error) { + return "No web module"; + } + return null; + }, sorter: (a, b) => Number(Boolean(a.package)) - Number(Boolean(b.package)), sortOrder: tableState.sortOrder("loaded"), responsive: ["sm"], diff --git a/test/controller/routes.js b/test/controller/routes.js index 5e764214..726b6aa6 100644 --- a/test/controller/routes.js +++ b/test/controller/routes.js @@ -102,7 +102,7 @@ describe("controller/src/routes", function() { }); it("should report missing manifest and entrypoint", async function() { controller.pluginInfos = [ - { name: "foo", version: "1.0.0", npmPackage: "foo" }, + { name: "foo", version: "1.0.0", npmPackage: "foo", webEntrypoint: "dist/web/index.js" }, { name: "bar", version: "1.0.0", npmPackage: "bar", manifest: {} }, { name: "baz", version: "1.0.0", npmPackage: "baz", manifest: { "baz.js": "remoteEntry.js" } }, ]; @@ -115,6 +115,19 @@ describe("controller/src/routes", function() { assert.equal(data[1].loaded, false); assert.equal(data[1].enabled, false); }); + it("should not report a missing manifest for plugins without a web build", async function() { + controller.pluginInfos = [ + { name: "foo", version: "1.0.0", npmPackage: "foo", hostEntrypoint: "dist/node/host.js" }, + { name: "bar", version: "1.0.0", npmPackage: "bar", controllerEntrypoint: "dist/node/controller.js" }, + { name: "baz", version: "1.0.0", npmPackage: "baz", instanceConfigFields: {} }, + ]; + let response = await fetch(endpoint); + assert.equal(response.status, 200); + let data = await response.json(); + assert.deepEqual(data[0].web, {}); + assert.equal(data[1].web.error, "Missing dist/web/manifest.json"); + assert.equal(data[2].web.error, "Missing dist/web/manifest.json"); + }); }); describe("/api/cluster-name", function() { let endpoint; diff --git a/test/lib/plugin.js b/test/lib/plugin.js index b9fd11ac..bfbe58f9 100644 --- a/test/lib/plugin.js +++ b/test/lib/plugin.js @@ -5,6 +5,26 @@ import * as lib from "@clusterio/lib"; describe("lib/plugin", function() { + describe("pluginNeedsWebBuild()", function() { + it("should be false for plugins with only host, instance or ctl code", function() { + assert.equal(lib.pluginNeedsWebBuild({ name: "foo", title: "Foo" }), false); + assert.equal(lib.pluginNeedsWebBuild({ + name: "foo", title: "Foo", hostEntrypoint: "host.js", instanceEntrypoint: "instance.js", + ctlEntrypoint: "ctl.js", messages: [], + }), false); + }); + it("should be true for plugins with web or controller code or config fields", function() { + assert.equal(lib.pluginNeedsWebBuild({ name: "foo", title: "Foo", webEntrypoint: "web.js" }), true); + assert.equal(lib.pluginNeedsWebBuild({ + name: "foo", title: "Foo", controllerEntrypoint: "controller.js", + }), true); + for (const field of [ + "controllerConfigFields", "hostConfigFields", "instanceConfigFields", "controlConfigFields", + ]) { + assert.equal(lib.pluginNeedsWebBuild({ name: "foo", title: "Foo", [field]: {} }), true, field); + } + }); + }); describe("invokeHook()", function() { let betaTestCalled = false; let plugins = new Map([ From c2325a8b6e06be219b89a576c17a6026ac9bfeba Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Sat, 12 Sep 2026 02:12:49 +0100 Subject: [PATCH 22/22] Add missing `as const` for permission string --- packages/web_ui/src/components/NpmButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web_ui/src/components/NpmButton.tsx b/packages/web_ui/src/components/NpmButton.tsx index 17f1883c..e543cb43 100644 --- a/packages/web_ui/src/components/NpmButton.tsx +++ b/packages/web_ui/src/components/NpmButton.tsx @@ -139,7 +139,7 @@ export function NpmButton(props: { } }, [hostId]); - const coreUpdatePermission = `core.${props.target === "controller" ? "controller" : "host"}.update`; + const coreUpdatePermission = `core.${props.target === "controller" ? "controller" : "host"}.update` as const; function onValuesChange({ action } : { action?: string }) { if (action) {