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.
This commit is contained in:
Danielv123 committed 2026-10-11 13:22:14 +02:00
1 parent b411c5bef8
commit 6e36c02ade
2 files changed
+46 -3

No files matched your search

+3 -2
View File
@@ -51,8 +51,9 @@ export async function loadPluginInfos(pluginList: Map<string, string>) {
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> = {
...context,
plugin: pluginInfo,
+43 -1
View File
@@ -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() {