From 2d1bf0ec12abd98f01188c4dad9b58d25d58db63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filiph=20Siitam=20Sandstr=C3=B6m?= Date: Sat, 3 May 2025 04:46:55 +0200 Subject: [PATCH 1/2] feat: migrate plugins from `require` to dynamic imports --- .changeset/plenty-kiwis-cheat.md | 5 +++ devapp/src/index.ts | 13 +++++--- packages/plugins/src/commands/command.ts | 6 ++-- packages/plugins/src/enums/priority.ts | 2 -- packages/plugins/src/pipeline.ts | 42 +++++++++--------------- 5 files changed, 31 insertions(+), 37 deletions(-) create mode 100644 .changeset/plenty-kiwis-cheat.md 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/devapp/src/index.ts b/devapp/src/index.ts index ab84e108e..6bf02c887 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 f04b692c2..5c581509a 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, @@ -25,13 +24,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 +119,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 @@ -147,7 +141,7 @@ class Pipeline { ); // Array to hold the ordered plugins - const orderedPlugins: Plugin[] = [] + const orderedPlugins: Array = []; // Iterate over all the bundled plugins, and import them for (const bundle of bundles) { @@ -173,7 +167,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; @@ -268,7 +262,6 @@ class Pipeline { ) as PluginPackage; // Get the main entry point for the plugin - // const main = resolve(path, this.esm ? manifest.module : manifest.main); const main = resolve(path, manifest.main); // Check if the provided entry point is valid @@ -282,7 +275,7 @@ class Pipeline { } // Import the plugin module - const module = require(resolve(path, main)); + const module = (await import(resolve(path, main))).default; // Get the plugin class from the module const plugin = module.default as Plugin; @@ -311,7 +304,6 @@ class Pipeline { // Push the plugin to the ordered plugins array orderedPlugins.push(plugin); - } catch (reason) { // Log the error this.logger.error( @@ -334,7 +326,6 @@ class Pipeline { return 0; }); - // Iterate over all the plugins, and initialize them for (const plugin of orderedPlugins) { // Initialize the plugin, and bind the events @@ -365,7 +356,7 @@ class Pipeline { } } - public reload(plugin: Plugin): void { + public async reload(plugin: Plugin): Promise { // Shut down the plugin plugin.onShutDown(plugin); @@ -384,23 +375,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; + + // Get the main entry point for the plugin + const main = resolve(path, manifest.main); - const module = require(path); + // 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; From 42d7fd6a9cd5913a5926d43ae1da3b9d40b33221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filiph=20Siitam=20Sandstr=C3=B6m?= Date: Sat, 3 May 2025 06:37:21 +0200 Subject: [PATCH 2/2] feat: import plugins asynchronously --- .changeset/two-numbers-deliver.md | 5 ++ packages/plugins/src/pipeline.ts | 123 +++++++++++++++--------------- 2 files changed, 67 insertions(+), 61 deletions(-) create mode 100644 .changeset/two-numbers-deliver.md 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/packages/plugins/src/pipeline.ts b/packages/plugins/src/pipeline.ts index 5c581509a..f549ecb1a 100644 --- a/packages/plugins/src/pipeline.ts +++ b/packages/plugins/src/pipeline.ts @@ -7,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"; @@ -242,76 +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, 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 = (await import(resolve(path, main))).default; + // 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) => {