diff --git a/.changeset/plenty-kiwis-cheat.md b/.changeset/plenty-kiwis-cheat.md new file mode 100644 index 000000000..087aeb269 --- /dev/null +++ b/.changeset/plenty-kiwis-cheat.md @@ -0,0 +1,5 @@ +--- +"@serenityjs/plugins": patch +--- + +Refactor plugin system to use dynamic imports. diff --git a/.changeset/two-numbers-deliver.md b/.changeset/two-numbers-deliver.md new file mode 100644 index 000000000..e7e492805 --- /dev/null +++ b/.changeset/two-numbers-deliver.md @@ -0,0 +1,5 @@ +--- +"@serenityjs/plugins": patch +--- + +Import plugins asynchronously. diff --git a/devapp/src/index.ts b/devapp/src/index.ts index f7ef0caba..e83c6b8ae 100644 --- a/devapp/src/index.ts +++ b/devapp/src/index.ts @@ -12,10 +12,13 @@ const serenity = new Serenity({ }); // Create a new plugin pipeline -new Pipeline(serenity, { path: "./plugins" }); +const pipeline = new Pipeline(serenity, { path: "./plugins" }); -// Register the LevelDBProvider -serenity.registerProvider(LevelDBProvider, { path: "./worlds" }); +// Initialize the pipeline +pipeline.initialize().then(() => { + // Register the LevelDBProvider + serenity.registerProvider(LevelDBProvider, { path: "./worlds" }); -// Start the server -serenity.start(); + // Start the server + return serenity.start(); +}); diff --git a/packages/plugins/src/commands/command.ts b/packages/plugins/src/commands/command.ts index c4ddad1a4..82d75b067 100644 --- a/packages/plugins/src/commands/command.ts +++ b/packages/plugins/src/commands/command.ts @@ -53,8 +53,8 @@ const register = (world: World, pipeline: Pipeline) => { action: PluginActionsEnum, plugin: PluginsEnum }, - ({ action, plugin }) => { - // Check if the action is reload + async ({ action, plugin }) => { + // Check if the action is reload or bundle if (action.result !== "reload" && action.result !== "bundle") return; // Get the plugin from the pipeline @@ -67,7 +67,7 @@ const register = (world: World, pipeline: Pipeline) => { // Check if the action is reload if (action.result === "reload") { // Reload the plugin - pipeline.reload(pluginInstance); + await pipeline.reload(pluginInstance); // Send the message to the origin return { diff --git a/packages/plugins/src/enums/priority.ts b/packages/plugins/src/enums/priority.ts index 3492d4b51..0338901b0 100644 --- a/packages/plugins/src/enums/priority.ts +++ b/packages/plugins/src/enums/priority.ts @@ -7,7 +7,6 @@ * The default priority is Normal. */ enum PluginPriority { - /** * A low priority plugin will be loaded after other plugins. * Useful for reacting to changes or final adjustments. @@ -27,5 +26,4 @@ enum PluginPriority { High } - export { PluginPriority }; diff --git a/packages/plugins/src/pipeline.ts b/packages/plugins/src/pipeline.ts index a54972e1c..f549ecb1a 100644 --- a/packages/plugins/src/pipeline.ts +++ b/packages/plugins/src/pipeline.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-require-imports */ import { existsSync, mkdirSync, @@ -8,6 +7,7 @@ import { unlinkSync, writeFileSync } from "node:fs"; +import { readdir, readFile } from "node:fs/promises"; import { relative, resolve } from "node:path"; import { deflateSync, inflateSync } from "node:zlib"; import { execSync } from "node:child_process"; @@ -25,13 +25,11 @@ import { PluginType } from "./enums"; interface PipelineProperties { path: string; commands: boolean; - initialize: boolean; } const DefaultPipelineProperties: PipelineProperties = { path: "./plugins", - commands: true, - initialize: true + commands: true }; class Pipeline { @@ -122,15 +120,12 @@ class Pipeline { .registerTrait(...plugin.entities.traits); } }); - - // Check if the plugins should be initialized - if (this.properties.initialize) this.initialize(); } /** * Initializes the plugins pipeline. */ - public initialize(): void { + public async initialize(): Promise { // Check if the plugins directory exists if (!existsSync(resolve(this.path))) // If not, create the plugins directory @@ -173,7 +168,7 @@ class Pipeline { writeFileSync(tempPath, index); // Import the plugin module - const module = require(tempPath); + const module = (await import(tempPath)).default; // Get the plugin class from the module const plugin = module.default as Plugin; @@ -248,77 +243,76 @@ class Pipeline { } // Filter out all the directories from the entries - const directories = readdirSync(resolve(this.path), { - withFileTypes: true - }).filter((dirent) => dirent.isDirectory()); - - // Iterate over all the directories, checking if they are valid plugins - for (const directory of directories) { - // Attempt to load the plugin - try { - // Get the path to the plugin - const path = resolve(this.path, directory.name); - - // Check if the plugin has a package.json file, if not, skip the plugin - if (!existsSync(resolve(path, "package.json"))) continue; + const directories = ( + await readdir(resolve(this.path), { + withFileTypes: true + }) + ).filter((dirent) => dirent.isDirectory()); + + // Iterate over all the directories, checking if they are valid plugins using Promise.all + await Promise.all( + directories.map(async (directory) => { + // Attempt to load the plugin + try { + // Get the path to the plugin + const path = resolve(this.path, directory.name); + + // Check if the plugin has a package.json file, if not, skip the plugin + if (!existsSync(resolve(path, "package.json"))) return; + + // Read the package.json file + const manifest = JSON.parse( + await readFile(resolve(path, "package.json"), "utf-8") + ) as PluginPackage; + + // Get the main entry point for the plugin + const main = resolve(path, manifest.main); + + // Check if the provided entry point is valid + if (!existsSync(resolve(path, main))) { + this.logger.warn( + `Unable to load plugin §1${manifest.name}§8@§1${manifest.version}§r, the main entry path "§8${relative(process.cwd(), resolve(path, main))}§r" was not found in the directory.` + ); + return; + } - // Read the package.json file - const manifest = JSON.parse( - readFileSync(resolve(path, "package.json"), "utf-8") - ) as PluginPackage; + // Import the plugin module + const module = (await import(resolve(path, main))).default; - // Get the main entry point for the plugin - // const main = resolve(path, this.esm ? manifest.module : manifest.main); - const main = resolve(path, manifest.main); + // Get the plugin class from the module + const plugin = module.default as Plugin; - // Check if the provided entry point is valid - if (!existsSync(resolve(path, main))) { - this.logger.warn( - `Unable to load plugin §1${manifest.name}§8@§1${manifest.version}§r, the main entry path "§8${relative(process.cwd(), resolve(path, main))}§r" was not found in the directory.` - ); + // Check if the plugin has already been loaded + if (this.plugins.has(plugin.identifier)) { + this.logger.warn( + `Unable to load plugin §1${plugin.identifier}§r, the plugin is already loaded in the pipeline.` + ); + return; + } - // Skip the plugin - continue; - } + // Set the pipeline, serenity, and path for the plugin + plugin.pipeline = this; + plugin.serenity = this.serenity; + plugin.path = path; + plugin.isBundled = false; - // Import the plugin module - const module = require(resolve(path, main)); + // Add the plugin to the plugins map + this.plugins.set(plugin.identifier, plugin); - // Get the plugin class from the module - const plugin = module.default as Plugin; + // Add the plugin to the plugins enum + PluginsEnum.options.push(plugin.identifier); - // Check if the plugin has already been loaded - if (this.plugins.has(plugin.identifier)) { - this.logger.warn( - `Unable to load plugin §1${plugin.identifier}§r, the plugin is already loaded in the pipeline.` + // Push the plugin to the ordered plugins array + orderedPlugins.push(plugin); + } catch (reason) { + // Log the error + this.logger.error( + `Failed to load plugin from "${relative(process.cwd(), resolve(this.path, directory.name))}", skipping the plugin.`, + reason ); - - // Skip the plugin - continue; } - - // Set the pipeline, serenity, and path for the plugin - plugin.pipeline = this; - plugin.serenity = this.serenity; - plugin.path = path; - plugin.isBundled = false; - - // Add the plugin to the plugins map - this.plugins.set(plugin.identifier, plugin); - - // Add the plugin to the plugins enum - PluginsEnum.options.push(plugin.identifier); - - // Push the plugin to the ordered plugins array - orderedPlugins.push(plugin); - } catch (reason) { - // Log the error - this.logger.error( - `Failed to load plugin from "${relative(process.cwd(), resolve(this.path, directory.name))}", skipping the plugin.`, - reason - ); - } - } + }) + ); // Sort the plugins by their priority orderedPlugins.sort((a, b) => { @@ -363,7 +357,7 @@ class Pipeline { } } - public reload(plugin: Plugin): void { + public async reload(plugin: Plugin): Promise { // Shut down the plugin plugin.onShutDown(plugin); @@ -382,23 +376,20 @@ class Pipeline { // Attempt to load the plugin try { - // Import the plugin module + // Read the package.json file + const manifest = JSON.parse( + readFileSync(resolve(path, "package.json"), "utf-8") + ) as PluginPackage; - const module = require(path); + // Get the main entry point for the plugin + const main = resolve(path, manifest.main); + + // Import the plugin module + const module = (await import(resolve(path, main))).default; // Get the plugin class from the module const rPlugin = module.default as Plugin; - // Check if the plugin is an instance of the Plugin class - if (!(rPlugin instanceof Plugin)) { - this.logger.warn( - `Unable to reload plugin from §8${relative(process.cwd(), path)}§r, the plugin is not an instance of the Plugin class.` - ); - - // Skip the plugin - return; - } - // Set the pipeline, serenity, and path for the plugin rPlugin.pipeline = this; rPlugin.serenity = this.serenity;