mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
232 lines
7.2 KiB
TypeScript
232 lines
7.2 KiB
TypeScript
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 vm from "vm";
|
|
|
|
/**
|
|
* Searches for local plugins in the plugins/ and external_plugins/ directories
|
|
* and adds them to the plugin list if they contain a package.json file.
|
|
*
|
|
* @param pluginList - Map of plugin names to their file paths
|
|
* @returns Promise that resolves to true if any plugins were added to the list, false otherwise
|
|
*/
|
|
async function findLocalPlugins(pluginList: Map<string, string>): Promise<boolean> {
|
|
// Check folders for plugins
|
|
const pluginFolders = [
|
|
"plugins",
|
|
"external_plugins",
|
|
];
|
|
|
|
let hasChanged = 0;
|
|
const ignoredFolders = new Set(["node_modules", ".git", "dist", "build", "disabled"]);
|
|
const maxDepth = 3;
|
|
|
|
for (const pluginFolder of pluginFolders) {
|
|
let pluginNames;
|
|
try {
|
|
pluginNames = await fs.readdir(pluginFolder);
|
|
} catch (err: any) {
|
|
if (err.code === "ENOENT") {
|
|
// Skip if folder doesn't exist
|
|
continue;
|
|
}
|
|
throw err;
|
|
}
|
|
for (const pluginName of pluginNames) {
|
|
const pluginPath = path.join(pluginFolder, pluginName);
|
|
if (pluginList.has(pluginName)) {
|
|
// If the one in the list is not the same as this one, warn
|
|
if (pluginList.get(pluginName) !== path.resolve(pluginPath)) {
|
|
logger.warn(
|
|
`${pluginName} is already in the plugin list, but with a different path - ` +
|
|
`using ${pluginList.get(pluginName)}`
|
|
);
|
|
}
|
|
continue;
|
|
}
|
|
const stats = await fs.stat(pluginPath);
|
|
if (stats.isDirectory()) {
|
|
// Check for package.json and plugin keyword
|
|
if (await checkPackageJson(pluginPath)) {
|
|
pluginList.set(pluginName, path.resolve(pluginPath));
|
|
logger.info(`Added ${pluginName} from ${pluginFolder}`);
|
|
hasChanged += 1;
|
|
continue;
|
|
}
|
|
|
|
if (pluginPath.split(path.sep).length < maxDepth && !ignoredFolders.has(pluginName)) {
|
|
logger.warn(`Possible monorepo detected, scanning ${pluginPath} for clusterio-plugins`);
|
|
pluginFolders.push(pluginPath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return hasChanged > 0;
|
|
}
|
|
|
|
function getPluginName(requireSpec: string) {
|
|
const context = vm.createContext({ require: require, pluginInfo: null });
|
|
const code = `pluginInfo = require(${JSON.stringify(requireSpec)}).plugin;`;
|
|
vm.runInContext(code, context);
|
|
return context.pluginInfo.name;
|
|
}
|
|
|
|
function resolvePackagePath(packageName: string, fromPath: string): string | undefined {
|
|
const packageRequire = createRequire(path.join(fromPath, "package.json"));
|
|
try {
|
|
return path.dirname(packageRequire.resolve(`${packageName}/package.json`));
|
|
} catch (err: any) {
|
|
if (err.code === "MODULE_NOT_FOUND" || err.code === "ERR_PACKAGE_PATH_NOT_EXPORTED") {
|
|
return undefined;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find NPM packages in the root package.json that satisfies the requirements for plugins.
|
|
* Adds discovered plugins to the plugin list.
|
|
*
|
|
* @param pluginList - Map of plugin names to their file paths
|
|
* @returns Promise that resolves to true if any plugins were added to the list, false otherwise
|
|
*/
|
|
async function findNpmPlugins(pluginList: Map<string, string>): Promise<boolean> {
|
|
let dependencies;
|
|
try {
|
|
const rootPackageJson = JSON.parse(await fs.readFile(path.resolve("package.json"), { encoding: "utf8" }));
|
|
dependencies = rootPackageJson.dependencies;
|
|
} catch (err: any) {
|
|
if (err.code === "ENOENT") {
|
|
// Skip if package.json doesn't exist
|
|
return false;
|
|
}
|
|
throw err;
|
|
}
|
|
|
|
if (!dependencies) {
|
|
return false;
|
|
}
|
|
|
|
let hasChanged = 0;
|
|
const pendingPackages = Object.entries(dependencies).map(([packageName, packageVersion]) => ({
|
|
packageName,
|
|
packageVersion,
|
|
parentPath: process.cwd(),
|
|
isRootDependency: true,
|
|
}));
|
|
const corePackages = new Set([
|
|
"@clusterio/controller",
|
|
"@clusterio/ctl",
|
|
"@clusterio/host",
|
|
"@clusterio/lib",
|
|
"@clusterio/web_ui",
|
|
]);
|
|
const visitedPackages = new Set<string>();
|
|
|
|
while (pendingPackages.length) {
|
|
const { packageName, packageVersion, parentPath, isRootDependency } = pendingPackages.shift()!;
|
|
if (visitedPackages.has(packageName)) {
|
|
continue;
|
|
}
|
|
|
|
const npmPackagePath = resolvePackagePath(packageName, parentPath);
|
|
if (!npmPackagePath) {
|
|
continue;
|
|
}
|
|
|
|
visitedPackages.add(packageName);
|
|
|
|
if (await checkPackageJson(npmPackagePath)) {
|
|
const pluginName = getPluginName(npmPackagePath);
|
|
if (pluginList.has(pluginName)) {
|
|
logger.warn(
|
|
`${pluginName} provided by ${packageName}@${packageVersion} is already in the plugin list, ` +
|
|
`but with a different path - using ${pluginList.get(pluginName)}`
|
|
);
|
|
} else {
|
|
pluginList.set(pluginName, packageName);
|
|
logger.info(`Added ${pluginName} from node modules`);
|
|
hasChanged += 1;
|
|
}
|
|
|
|
const packageJson = JSON.parse(
|
|
await fs.readFile(path.join(npmPackagePath, "package.json"), { encoding: "utf8" })
|
|
);
|
|
for (const [dependencyName, dependencyVersion] of Object.entries(packageJson.dependencies ?? {})) {
|
|
pendingPackages.push({
|
|
packageName: dependencyName,
|
|
packageVersion: dependencyVersion,
|
|
parentPath: npmPackagePath,
|
|
isRootDependency: false,
|
|
});
|
|
}
|
|
} else if (isRootDependency && !corePackages.has(packageName)) {
|
|
logger.warn(`${packageName}@${packageVersion} is not a clusterio-plugin`);
|
|
}
|
|
}
|
|
return hasChanged > 0;
|
|
}
|
|
|
|
/**
|
|
* Loads the plugin list from the specified file and optionally discovers local plugins
|
|
*
|
|
* @param pluginListPath - Path to the plugin list JSON file
|
|
* @param findLocal - Whether to look for local plugins in plugins/ and external_plugins/ directories
|
|
* @returns Map of plugin name to plugin path
|
|
*/
|
|
export async function loadPluginList(pluginListPath: string, findLocal = true): Promise<Map<string, string>> {
|
|
let pluginList = new Map<string, string>();
|
|
|
|
// Try to load existing plugin list
|
|
try {
|
|
pluginList = new Map(JSON.parse(await fs.readFile(pluginListPath, { encoding: "utf8" })));
|
|
logger.info(`Loaded ${pluginList.size} plugins from ${pluginListPath}`);
|
|
} catch (err: any) {
|
|
if (err.code !== "ENOENT") {
|
|
throw err;
|
|
}
|
|
logger.info(`No existing plugin list found at ${pluginListPath}`);
|
|
}
|
|
|
|
// Track if any changes were made
|
|
let hasChanges = false;
|
|
|
|
// Optionally discover local plugins
|
|
if (findLocal) {
|
|
hasChanges = await findLocalPlugins(pluginList) || hasChanges;
|
|
}
|
|
|
|
// Discover NPM plugins
|
|
hasChanges = await findNpmPlugins(pluginList) || hasChanges;
|
|
|
|
if (hasChanges) {
|
|
await libFileOps.safeOutputFile(pluginListPath, JSON.stringify([...pluginList], null, "\t"));
|
|
}
|
|
|
|
return pluginList;
|
|
}
|
|
|
|
/**
|
|
* Helper function to check if a package.json file exists and contains the clusterio-plugin keyword
|
|
* @param pluginPath - Path to the plugin directory
|
|
* @returns Promise that resolves to true if package.json exists and has clusterio-plugin keyword
|
|
*/
|
|
async function checkPackageJson(pluginPath: string): Promise<boolean> {
|
|
const packageJsonPath = path.join(pluginPath, "package.json");
|
|
|
|
// Read and parse package.json
|
|
let content;
|
|
try {
|
|
content = await fs.readFile(packageJsonPath, { encoding: "utf8" });
|
|
} catch (err: any) {
|
|
return false;
|
|
}
|
|
const packageJson = JSON.parse(content);
|
|
|
|
// Check if package has the clusterio-plugin keyword
|
|
return packageJson.keywords?.includes("clusterio-plugin");
|
|
}
|