mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
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.
233 lines
7.2 KiB
TypeScript
233 lines
7.2 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import { createRequire } from "node:module";
|
|
import path from "path";
|
|
import { logger } from "./logging.js";
|
|
import * as libFileOps from "./file_ops.js";
|
|
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;
|
|
}
|
|
|
|
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;`;
|
|
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");
|
|
}
|