From 6662bfca4fc8eec7a775acbeb45fa4bc1145a9b6 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Fri, 12 Jun 2026 21:09:23 +0100 Subject: [PATCH] Skip invalid plugins entries for help and plugin commands --- docs/guides/upgrading-to-v4.md | 2 + lib/classes/plugin-manager.js | 28 ++++++++----- test/unit/lib/classes/plugin-manager.test.js | 43 ++++++++++++++++++++ 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/docs/guides/upgrading-to-v4.md b/docs/guides/upgrading-to-v4.md index ccf03515f..80a2486a0 100644 --- a/docs/guides/upgrading-to-v4.md +++ b/docs/guides/upgrading-to-v4.md @@ -111,6 +111,8 @@ Versioned plugin configuration entries such as `example-osls-plugin@1.2.3` now f The legacy `plugins.localPath` option is still supported, but module names loaded from that directory must use npm package-name syntax. If you previously loaded uppercase local plugin names such as `ServicePluginMock1` through `.serverless_plugins` or `plugins.localPath`, rename them to lowercase npm-style names or reference them with explicit `./` local paths. +`serverless --help` and the `plugin` management commands skip invalid entries with a warning, so you can still inspect the service and fix the configuration. + ### `plugin install` accepts stricter package specs `serverless plugin install --name` now accepts only npm package names with optional semver ranges or npm dist-tags. Embedded literal quotes are rejected; quote the whole `--name` value at the shell level when the version range contains spaces or shell metacharacters: diff --git a/lib/classes/plugin-manager.js b/lib/classes/plugin-manager.js index b64de57b4..48c1aade1 100644 --- a/lib/classes/plugin-manager.js +++ b/lib/classes/plugin-manager.js @@ -223,7 +223,13 @@ class PluginManager { } async resolveServicePlugins(servicePlugs) { - const pluginsObject = this.parsePluginsObject(servicePlugs); + const arePluginLoadFailuresTolerated = + this.serverless.processedInput.isHelpRequest || + this.cliOptions.help || + this.pluginIndependentCommands.has(this.cliCommands[0]); + const pluginsObject = this.parsePluginsObject(servicePlugs, { + tolerateInvalidEntries: arePluginLoadFailuresTolerated, + }); const serviceDir = this.serverless.serviceDir; const pluginNames = pluginsObject.modules @@ -240,11 +246,7 @@ class PluginManager { if (error.code !== 'PLUGIN_NOT_FOUND') throw error; // Plugin not installed - if ( - this.serverless.processedInput.isHelpRequest || - this.cliOptions.help || - this.pluginIndependentCommands.has(this.cliCommands[0]) - ) { + if (arePluginLoadFailuresTolerated) { // User may intend to install plugins just listed in serverless config // Therefore skip on MODULE_NOT_FOUND case continue; @@ -301,7 +303,7 @@ class PluginManager { .filter((v) => v !== null); } - parsePluginsObject(servicePlugs) { + parsePluginsObject(servicePlugs, { tolerateInvalidEntries = false } = {}) { let localPath = this.serverless && this.serverless.serviceDir && @@ -320,9 +322,15 @@ class PluginManager { } } - modules = modules.map((entry) => - validateConfiguredPluginReference(entry, this.serverless.serviceDir) - ); + modules = modules.flatMap((entry) => { + try { + return [validateConfiguredPluginReference(entry, this.serverless.serviceDir)]; + } catch (error) { + if (!tolerateInvalidEntries) throw error; + log.warning(`Ignoring invalid "plugins" configuration entry: ${error.message}`); + return []; + } + }); return { modules, localPath }; } diff --git a/test/unit/lib/classes/plugin-manager.test.js b/test/unit/lib/classes/plugin-manager.test.js index 2decb3729..76656c7f6 100644 --- a/test/unit/lib/classes/plugin-manager.test.js +++ b/test/unit/lib/classes/plugin-manager.test.js @@ -684,6 +684,40 @@ describe('PluginManager', () => { ServerlessError ); }); + + it('should throw an error when trying to load invalid plugins entries', () => { + const servicePlugins = ['serverless-webpack@1.2.3', servicePluginMock1Name]; + + return expect(pluginManager.loadAllPlugins(servicePlugins)).to.be.rejected.then((error) => { + expect(error).to.have.property('code', 'INVALID_PLUGIN_REFERENCE'); + }); + }); + + it('should not throw error when trying to load invalid plugins entries with help flag', async () => { + const servicePlugins = ['serverless-webpack@1.2.3', servicePluginMock1Name]; + + pluginManager.setCliOptions({ help: true }); + + resolveInput.clear(); + return overrideArgv({ args: ['serverless', '--help'] }, async () => { + await pluginManager.loadAllPlugins(servicePlugins); + + expect( + pluginManager.plugins.some((plugin) => plugin instanceof ServicePluginMock1) + ).to.equal(true); + }); + }); + + it('should not throw error when running the plugin commands and plugins entries are invalid', async () => { + const servicePlugins = ['serverless-webpack@1.2.3', servicePluginMock1Name]; + pluginManager.setCliCommands(['plugin']); + + await pluginManager.loadAllPlugins(servicePlugins); + + expect(pluginManager.plugins.some((plugin) => plugin instanceof ServicePluginMock1)).to.equal( + true + ); + }); }); describe('#resolveServicePlugins()', () => { @@ -794,6 +828,15 @@ describe('PluginManager', () => { .to.throw() .with.property('code', 'INVALID_PLUGIN_REFERENCE'); }); + + it('drops invalid entries when tolerateInvalidEntries is set', () => { + const result = pluginManager.parsePluginsObject( + ['serverless-webpack@1.2.3', servicePluginMock1Name, './../plugin'], + { tolerateInvalidEntries: true } + ); + + expect(result.modules).to.deep.equal([servicePluginMock1Name]); + }); }); describe('command aliases', () => {