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 87d3cd72..ce4bb2b3 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/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/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/ 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() {