Plugin System is weird
See original GitHub issueI want to preface all of this with a big thank you to all maintainers of this wonderful project. Please don’t take anything of the following as negativity, but constructive criticism or questions, because of lacking documentation, which I am happy to PR, once I understand it.
Maybe it’s a matter of taste, but I find that the plugin system is quite weird. I guess there is some history (backwards compatibility) to it that I am missing, but here are my points.
Constructor Signature
The constructor signature for plugins is very weird.
Mutating the global config
First of all, I think this is a bug:
The original appConfig
object is mutated with every plugin that is loaded, since merge
mutates the first parameter.
This is a contrived example, but this config would thrash the middlewares
:
auth:
my-auth-plugin:
middlewares: []
enabled: true
middlewares:
a-very-important-middleware-plugin:
enabled: true
Since auth
plugins are loaded first and since their config is merged into the global config, middlewares
get overridden with an empty array []
and a-very-important-middleware-plugin
is never loaded.
Assuming we need to keep this weird behavior of merging the plugin config with the global config, it should actually look like this IMO:
function mergeConfig(appConfig, pluginConfig): Config {
return _.merge({}, appConfig, pluginConfig);
}
Different signature for ES module plugins
The next weird thing is that this only occurs for ES module based plugins:
“Old school” plugins only receive the plugin’s config:
esModuleBased = new Plugin(mergeConfig(config, pluginConfigs[pluginId]), params);
oldSchool = plugin(pluginConfigs[pluginId], params);
Is this intentional?
Inconsistent params
/ options
type
Different plugin types are invoked with different shapes of params
/ options
.
Auth & Middleware Plugins
Receive { config, logger }
.
Theme Plugins
Receive an empty object {}
.
TypeScript Types
The relevant TypeScript types look like this:
class Plugin<T> {
constructor(config: T, options: PluginOptions<T> );
}
interface IPlugin<T> {
version?: string;
// In case a plugin needs to be cleaned up/removed
close?(): void;
}
type PluginOptions<T> = {
config: T & Config;
logger: Logger
}
So according to this, I would expect to only receive my plugin’s config as the first parameter (config
) and always receive { config: GlobalAppConfig & PluginConfig, logger }
, as the second parameter.
Proposed Solution
Personally, I would prefer, if we’d remove the & PluginConfig
from this and remove the merging for the first parameter.
This way I can consume a clean plugin config, that will not accidentally get clobbered during the boot process, as the first parameter. And then as the second parameter I could access a clean global app config
, that is not weirdly mutated.
So kinda like this:
class Plugin<T> {
constructor(config: T, options: PluginOptions );
}
interface IPlugin<T> {
version?: string;
// In case a plugin needs to be cleaned up/removed
close?(): void;
}
type PluginOptions = {
config: Config;
logger: Logger
}
I guess that this wasn’t done like this, because of backwards compatibility. So maybe we can fix this with a major release?
Multiple Plugin Instances per Type
My second major issue is that, when a plugin intends to implement multiple plugin types, e.g. IPluginAuth
and IPluginMiddleware
, it will be instantiated twice.
This was not at all obvious from the documentation and I personally would prefer, if the instance was shared. I can workaround this by implementing a singleton pattern, but it needlessly complicated things.
FWIW here's my singleton plugin pattern.
- https://github.com/ClarkSource/verdaccio-oidc/blob/e5d9208c0d1d0d4834312b17a70aaa4089e5b3ac/index.js
- https://github.com/ClarkSource/verdaccio-oidc/blob/e5d9208c0d1d0d4834312b17a70aaa4089e5b3ac/src/index.ts
- https://github.com/ClarkSource/verdaccio-oidc/blob/e5d9208c0d1d0d4834312b17a70aaa4089e5b3ac/src/plugin/plugin.ts#L58-L112
Again, thanks a lot for this great project! I love working with it and hope that we can make this even better! 🎉
Issue Analytics
- State:
- Created 4 years ago
- Comments:5 (4 by maintainers)
Top GitHub Comments
Hi @buschtoens thanks for the detailed report, that’s is how should be done ☕️ .
The answer you are looking for is, yes, legacy. The story of this project started did not start with the best way, old Node.js supported, many PR without enough context and so on, object mutability, coercion and etc etc. We had to onboard many legacy users and developers from Sinopia in the last 2 major releases. I think nowadays if someone still using Sinopia, hard to believe, but still happens (https://twitter.com/jsunderhood/status/1151564931134894080), Verdaccio 4 is the last step for safe on-boarding. Verdaccio ~5~ 6 will have breaking changes in terms of plugins, for sure.
Some of the issues you have mentioned were already reported, https://github.com/verdaccio/verdaccio/issues/928 https://github.com/verdaccio/verdaccio/issues/1357 and I’m aware of them, but I just noticed other things that you pointed out.
So, that being said, as you mention, this only can be done in a major release, since
v4
was just released, the next one will happen until next year https://github.com/verdaccio/contributing/blob/master/RELEASES.md but usually we start with alphas in October, so, we might work in a branch a new approach for plugins.I did not have plans to start any development any time soon, I have https://github.com/verdaccio/verdaccio/issues/541 and https://github.com/verdaccio/verdaccio/issues/1334 in my priority list before go with this.
But I will take your suggestions very seriously and perhaps we can contribute on this on Verdaccio ~5~ 6, keep in touch 🤙 .
fixed https://github.com/verdaccio/verdaccio/pull/3370