From b411c5bef8809438012af2ca504638970b28f704 Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Sun, 11 Oct 2026 13:15:14 +0200 Subject: [PATCH 1/2] Fix running Clusterio from published packages Since the move to ES modules and pnpm 12, packages installed from npm fail to start: - The package imports of @clusterio/lib point Node at TypeScript sources, which Node refuses to strip types from inside node_modules. - The controller, host and ctl import their package.json from next to the compiled entrypoint, a copy pnpm leaves out of the package. - ctl, host and web_ui have no .npmignore, so pnpm packs them using the repository .gitignore and leaves out nearly all of dist/. Resolve the package imports to the compiled files at runtime while keeping the sources for type checking, import package.json by package name so it resolves to the real file, and add .npmignore files so pnpm packs the same files as npm. --- packages/controller/controller.ts | 2 +- packages/controller/src/WsServer.ts | 2 +- packages/ctl/.npmignore | 1 + packages/ctl/ctl.ts | 2 +- packages/host/.npmignore | 1 + packages/host/host.ts | 2 +- packages/lib/package.json | 10 ++++++++-- packages/web_ui/.npmignore | 1 + 8 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 packages/ctl/.npmignore create mode 100644 packages/host/.npmignore create mode 100644 packages/web_ui/.npmignore diff --git a/packages/controller/controller.ts b/packages/controller/controller.ts index 4736aba0..ba863ae9 100644 --- a/packages/controller/controller.ts +++ b/packages/controller/controller.ts @@ -32,7 +32,7 @@ import { ConsoleTransport, levels, logger } from "@clusterio/lib"; 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" }; +import packageConfig from "@clusterio/controller/package.json" with { type: "json" }; // globals let controller: Controller; diff --git a/packages/controller/src/WsServer.ts b/packages/controller/src/WsServer.ts index db582ec1..9e5b2dd4 100644 --- a/packages/controller/src/WsServer.ts +++ b/packages/controller/src/WsServer.ts @@ -12,7 +12,7 @@ import ControlConnection from "./ControlConnection.js"; import HostConnection from "./HostConnection.js"; import WsServerConnector from "./WsServerConnector.js"; -import packageConfig from "../package.json" with { type: "json" }; +import packageConfig from "@clusterio/controller/package.json" with { type: "json" }; const wsMessageCounter = new lib.Counter( "clusterio_controller_websocket_message_total", diff --git a/packages/ctl/.npmignore b/packages/ctl/.npmignore new file mode 100644 index 00000000..5bfde2b7 --- /dev/null +++ b/packages/ctl/.npmignore @@ -0,0 +1 @@ +# Without an .npmignore pnpm packs using the repository .gitignore, which leaves out dist/ diff --git a/packages/ctl/ctl.ts b/packages/ctl/ctl.ts index 55f2d944..0de27ee7 100755 --- a/packages/ctl/ctl.ts +++ b/packages/ctl/ctl.ts @@ -7,7 +7,7 @@ import fs from "node:fs/promises"; import yargs, { type Argv } from "yargs"; import path from "path"; -import packageConfig from "./package.json" with { type: "json" }; +import packageConfig from "@clusterio/ctl/package.json" with { type: "json" }; import { strict as assert } from "assert"; // Reduce startup time by lazy compiling schemas. diff --git a/packages/host/.npmignore b/packages/host/.npmignore new file mode 100644 index 00000000..5bfde2b7 --- /dev/null +++ b/packages/host/.npmignore @@ -0,0 +1 @@ +# Without an .npmignore pnpm packs using the repository .gitignore, which leaves out dist/ diff --git a/packages/host/host.ts b/packages/host/host.ts index cd050cb0..8d31e071 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 packageConfig from "./package.json" with { type: "json" }; +import packageConfig from "@clusterio/host/package.json" with { type: "json" }; import winston from "winston"; import "winston-daily-rotate-file"; diff --git a/packages/lib/package.json b/packages/lib/package.json index e23c4b8c..5f6f94b3 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -25,11 +25,17 @@ "type": "module", "imports": { "#WebSocket": { - "node": "./src/link/WebSocket.node.ts", + "node": { + "types": "./src/link/WebSocket.node.ts", + "default": "./dist/node/src/link/WebSocket.node.js" + }, "import": "./src/link/WebSocket.browser.ts" }, "#is_deep_strict_equal": { - "node": "./src/is_deep_strict_equal.node.ts", + "node": { + "types": "./src/is_deep_strict_equal.node.ts", + "default": "./dist/node/src/is_deep_strict_equal.node.js" + }, "import": "./src/is_deep_strict_equal.browser.ts" } }, diff --git a/packages/web_ui/.npmignore b/packages/web_ui/.npmignore new file mode 100644 index 00000000..5bfde2b7 --- /dev/null +++ b/packages/web_ui/.npmignore @@ -0,0 +1 @@ +# Without an .npmignore pnpm packs using the repository .gitignore, which leaves out dist/ From 6e36c02adedd944538c5309cdff2dbd19eca0b08 Mon Sep 17 00:00:00 2001 From: Danielv123 Date: Sun, 11 Oct 2026 13:22:14 +0200 Subject: [PATCH 2/2] Load plugins installed from npm by package name Plugins installed from npm are listed by package name, but the plugin loader joined that name onto the main and entrypoint paths and imported the result as a file, which resolves relative to the working directory. Every npm installed plugin then failed to load. Resolve these paths from the directory of the plugin's package.json instead. --- packages/lib/src/plugin_loader.ts | 5 ++-- test/lib/plugin_loader.js | 44 ++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/plugin_loader.ts b/packages/lib/src/plugin_loader.ts index 818efff1..d0b8c400 100644 --- a/packages/lib/src/plugin_loader.ts +++ b/packages/lib/src/plugin_loader.ts @@ -51,8 +51,9 @@ export async function loadPluginInfos(pluginList: Map) { try { pluginPackage = (await import(pathToFileURL(packagePath).href, { with: { type: "json" }})).default; + // pluginPath is a package name for plugins installed from npm, so resolve from the package itself pluginInfo = (await import( - pathToFileURL(path.posix.join(pluginPath, pluginPackage.main ?? "index.js")).href + pathToFileURL(path.join(path.dirname(packagePath), pluginPackage.main ?? "index.js")).href )).plugin; } catch (err: any) { if (err.code === "InstallationError") { @@ -106,7 +107,7 @@ export async function loadPlugin< return; } - const module = await import(pathToFileURL(path.posix.join(pluginInfo.requirePath, requirePath)).href); + const module = await import(pathToFileURL(path.join(path.dirname(pluginInfo.packagePath), requirePath)).href); const pluginContext: libPlugin.PluginLoadContext = { ...context, plugin: pluginInfo, diff --git a/test/lib/plugin_loader.js b/test/lib/plugin_loader.js index 412638ff..ec08f515 100644 --- a/test/lib/plugin_loader.js +++ b/test/lib/plugin_loader.js @@ -66,6 +66,43 @@ describe("lib/plugin_loader", function() { { message: `PluginError: ${brokenMessage}` } ); }); + describe("plugin installed from npm", function() { + // Plugins from npm are listed by package name, so they must be resolved like a dependency + const packageDir = path.resolve("node_modules", "@clusterio-test", "npm_plugin"); + before(async function() { + await fs.mkdir(path.join(packageDir, "dist"), { recursive: true }); + await fs.writeFile(path.join(packageDir, "package.json"), JSON.stringify({ + name: "@clusterio-test/npm_plugin", version: "0.0.2", main: "dist/index.js", type: "module", + })); + await fs.writeFile( + path.join(packageDir, "dist", "index.js"), + 'export const plugin = { name: "npm_plugin", controllerEntrypoint: "dist/controller.js" };', + ); + await fs.writeFile( + path.join(packageDir, "dist", "controller.js"), + "export default async function(context) { context.controller.loadedNpmPlugin = true; }", + ); + }); + after(async function() { + await fs.rm(path.dirname(packageDir), { recursive: true, force: true }); + }); + + it("should load the plugin and its entrypoints", async function() { + const [pluginInfo] = await lib.loadPluginInfos( + new Map([["npm_plugin", "@clusterio-test/npm_plugin"]]) + ); + assert.equal(pluginInfo.name, "npm_plugin"); + assert.equal(pluginInfo.npmPackage, "@clusterio-test/npm_plugin"); + assert.equal(pluginInfo.packagePath, path.join(packageDir, "package.json")); + + const controller = new mock.MockController(); + await lib.loadPlugin( + pluginInfo, "controller", { controller, metrics: {}, logger: new mock.MockLogger() }, + "ControllerPlugin", BaseControllerPlugin, + ); + assert.equal(controller.loadedNpmPlugin, true); + }); + }); it("should reject on invalid plugin", async function() { await assert.rejects( lib.loadPluginInfos(new Map([["invalid", path.resolve(invalidPlugin)]])), @@ -122,7 +159,12 @@ describe("lib/plugin_loader", function() { }); function info(requirePath, entrypoint = "controller.js") { - return { name: "test", requirePath, controllerEntrypoint: entrypoint }; + return { + name: "test", + requirePath, + packagePath: path.join(requirePath, "package.json"), + controllerEntrypoint: entrypoint, + }; } it("should do nothing when the entrypoint is not set", async function() {