Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/guides/upgrading-to-v4.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
28 changes: 18 additions & 10 deletions lib/classes/plugin-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -301,7 +303,7 @@ class PluginManager {
.filter((v) => v !== null);
}

parsePluginsObject(servicePlugs) {
parsePluginsObject(servicePlugs, { tolerateInvalidEntries = false } = {}) {
let localPath =
this.serverless &&
this.serverless.serviceDir &&
Expand All @@ -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 };
}
Expand Down
43 changes: 43 additions & 0 deletions test/unit/lib/classes/plugin-manager.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
Loading