-
Notifications
You must be signed in to change notification settings - Fork 65
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Plugin System #514
Comments
About the relationship between The past/status-quo
With the framework, the game changes
What can we do about it?Here are some options
|
@tgriesser Any thoughts you have about the above comment are welcome! |
Took a deep dive with @Weakky about #514 (comment), and got aligned; Solution 1 - Dynamically pass core components to pluginsThis is what we think we should do. Major Wins
SchemaApp{
"dependencies": {
"@nexus/schema": "...",
"graphql": "...",
"nexus-schema-plugin-foo": "..."
}
} import { makeSchema } from '@nexus/schema'
import nexusSchemaPluginFoo from 'nexus-schema-plugin-foo'
const schema = makeSchema({
plugins: [
nexusSchemaPluginFoo()
],
// ...
})
// ... Plugin{
"name": "nexus-schema-plugin-foo",
"devDependencies": {
"@nexus/schema": "...",
}
} import type { Plugin } from '@nexus/schema'
interface Options {
// ...
}
const plugin: Plugin = (options: Options) => ({ lens, graphql, nexusSchema }) => {
// ...
};
export { Options }
export default plugin FrameworkApp{
"dependencies": {
"nexus": "...",
"nexus-plugin-foo": "..."
}
} PluginNotes
Minor Problems
{
"name": "nexus-plugin-foo",
"dependencies": {
"nexus-schema-plugin-foo": "...",
},
"devDependencies": {
"nexus": "...",
}
} import type { Plugin } from 'nexus/plugins'
interface Options {
// ...
}
const plugin: Plugin = (options: Options) => ({ lens, graphql, nexusSchema }) => {
// ...
};
export { Options }
export default plugin Solution 2 - Apps depend upon
|
Separate topic from previous comment but in that same deep dive with @Weakky. Design around how the framework plugins can express their dep on the framework. The following is not exact API but the gist of what we want to do. Framework plugins will use their devDep pin on the framework to supply at runtime their dep. fwiw this is akin to how the Fastify plugin system works. This is needed in lieu of not pinning in the dep graph #514 (comment) but also because it permits to provide a lot better feedback anyways for the user about the incompatibility––compared to if we relied solely on the package manager feedback (e.g. peer dep warnings are largely ignored by users etc.). import package from "../package.json";
// ...
// devDependencies: {
// "nexus": "^1.2.3",
// ...
// }
// ...
const plugin = () => {
// ...
return {
supports: package.devDependencies["@nexus/schema"]
};
}; If this system works out well, we'll go further and automate this so plugin authors 1) cannot screw it up 2) don't have to do it, think about it. |
Notes from call today with @Weakky See Raw Live Share Session Sketches
Building Packaged PluginsThe heart of the plugin interface could be a manifest that provides some basic metadata and location information about where modules can be found for each respective dimension. The reason for the module paths is that the runtime dimension of a plugin must be kept in separate from the others to facilitate tree shaking production builds. type Plugin = <Settings>(settings: Settings) => {
name: string
frameworkVersion: string, // valid npm version expression
settings: Settings
settingsType?: {
module: string // a path to TS module
export: string // a name of an export in the TS module for the Settings type
},
runtime?: {
module: string // a path to a compiled JS module
export: string // a name of an export in the JS module for the plugin dimension
},
worktime?: {
module: string // ^
export: string // ^
},
testtime?: {
module: string // ^
export: string // ^
},
} The signatures of the various dimensions are thus:
About Settings and Settings ModuleNote that the dimensions share a common settings data. A plugin feature might require collaboration between runtime and worktime, or testtime and worktime, etc. From a plugin consumer perspective, its one feature, they don't want to manage plugin settings across different dimensions. We think this is a hard usability requirement. The consequence of this is that the type of About Settings Being ReturnedThe settings must be returned to the framework. This may seem odd. The reason is that the framework takes responsibility for feeding settings to plugins. The reason for that is settings, better known as "configuration management" can get complex in real-world deployment scenarios; different stages of deployment will require different settings, and getting those settings actually set will have different ideal mechanisms, such as config files or environment variables. We want the framework to support a configuration cascade where users can count on e.g. being able to set via an env var any plugin option in a consistent and safely parsed way. There should be zero effort to "rig" this up by users and zero effort for plugin authors too. By everyone just following a few minimal conventions, things will Just Work (tm). Another reason we need settings returned is that at worktime and testtime we need to read the settings in from the user's source code. Having plugins return the settings is our only way to do that, assuming we want settings to be entered directly into plugin functions (e.g. we reject this: About ToolingThere is very little value for most plugin authors to manage the above manifest manually. Instead the Nexus CLI should have a build step that provides the following benefits:
The conventional layout could be:
The conventional export name would be If the plugin author insists on using another layout for some reason, we could support that via config in the package json, e.g.:
Note that this starts to look like the API all over again but the important difference is that it is for generating API boilerplate. And the important thing about having an API is that we don't lock out other ideas in the future or other tooling ideas from other users, etc. API has benefits of being an... API! Consuming Packaged Plugins
Consuming Inline PluginsBeing able to write a plugin inline inside an app is important. The constraints are different than with packages plugins:
First of all, a user can of course just do this: import { use } from 'nexus'
use({
name: "...",
frameworkVersion: "..."
// ...
}) But this is pretty verbose and the paths part quickly turns this into a nightmare that is too taxing for the quick "I'm curious" moments. A simpler API should be made availble: import { inlinePlugin } from 'nexus/plugins'
use(inlinePlugin({
runtime(lens) { /*...*/ },
worktimetime(lens) { /*...*/ },
testtime(lens) { /*...*/ },
})) This pattern must never ever be used by packaged plugins. The framework stance should be that measures, at any time, will be taken to actively block packaged plugins from using this pattern. There are various static and runtime tricks we could deploy to enforce it. Ideally though the benefits of following the conventions and general ecosystem cohesion will be good enough to make this a non-issue. None the less it should be clear that any packaged plugin trying to export itself using Sometimes a user might want to go a bit futher with their plugin, building what is effectively a packaged plugin yet also still local to their app. The farmework can support this via a special convention:
With this convention then, imagine this setup and example:
//app.ts
import { use } from 'nexus'
import { localPlugin } from 'nexus/plugins'
use(localPlugin('foo', {
// ..
})) Maybe we could use proxies instead: //app.ts
import { use } from 'nexus'
import { localPlugin } from 'nexus/plugins'
use(localPlugin.foo({
// ..
})) But actually, it seems the auto-discovery feature would be almost the same then, and already exists. Name collisions are possible (between their local and packaged plugins) but the user can always change the name of their local easily if needed. //app.ts
import { use } from 'nexus'
use.foo({
// ..
}) Consuming Component Pluginscomponents that are pluggable could accept plugins over a import { schema, log, server } from 'nexus'
import { superCoolScalar } from 'qux'
import { nyanCatTheme } from 'foo'
import { cors } from 'bar'
schema.use(superCoolScalar({
// ...
}))
log.use(nyanCatTheme({
// ...
}))
server.use(cors({
// ...
})) Component plugins lose the following benefits over framework plugins:
|
Tree Shaking PluginsBecause the plugin entrypoint is a set of paths using a custom/dynamic loader system, it means tree-shaking will not work by default. Specifically, it will think that the only thing a used by the app is the plugin manifest.
What tree shaking would think it needs to bring in (roughly):
To solve this the nexus start module could do this:
This information will come from |
https://github.com/graphql-nexus/nexus-plugin-prisma/runs/571935272 This error occurred b/c of mismatch of nexus versions between plugin and framework. I still don't understand the actual issue (config protected class blah blah) I think it has to do with the mix of nominally different classes, "same class" but different packages. We're lucky we can fix this as we own the framework and plugin, but this is a high bar to climb from a plugin ecosystem perspective. |
Noticed in #633 that another Nexus Schema plugin api part aside from plugin wrapper is |
Udpate
|
This issue is a WIP. The content here will be refined over time.
We've had feedback in the Nexus transition issue (1, 2), and committed to, keeping schema component usable on its own, and schema component plugins usable on their own, too.
This issue is a place to at least start design/discussion of the system we'll need to realize the commitment. Centralizing all the considerations into one place will help us design the system. It is very complex and tradeoffs are hard to analyze. It is easy to over-optimize a side at the expense of others, without noticing until it is too late, maybe a lot of work done that could have been avoided.
Capabilities
WIP - incomplete, sometimes ambiguous
makeSchema
of@nexus/schema
Questions
Social
Versioning
Worst: runtime crash and burn only under a set of production circumstances; Best: clear up front feedback while in dev mode?
Schema Component vs Framework
Integration
It should be practically impossible to have silently incompatible plugins. This means a case where framework user installs two plugins and their app stops working because of an integration problem caused by the combination of the two plugins. How?
We want the api to be built on top of the same internals that plugins are. This means anything the api let's users do is something that plugins can do, automate. That means a strong separation needs to exist in the API between escape hatches and official API surface that plugins rely on. This means for example that if a plugin
A
does (effectively, psudeo code):and plugin
B
does (effectively, psudeo code):And
req.foobar()
is broken bycreateSomeCustomServer
, then, bad.The features in the non-escape-hatch API need to be guaranteed compatible. Any features in
schema.addToContext
that rely on server need to be abstracted from the particulars of the actual server, andcreateSomeCustomServer
must be required to fulfill all the framework features that are touching the server.This is one example but the principal is far reaching, general.
Any plugin can augment the CLI. This means any CLI Invocation begins by loading the plugins that tap into it. How will we do that?
Should users have to enable the plugins explicitly after installing them?
Should plugins be imported or appear configurable via typegen?
If we go the auto-use way, how does that square with schema level plugins?
Do we enforce an npm package name for schema plugins so that they can be auto-used too?
nexus-schema-plugin-*
?Do we ignore them and say "publish a framework variant that wraps it"
Do we add a new API like:
If we had such an API, would one not expect too then (this is compatible with the above framework
use
above, for framework):The text was updated successfully, but these errors were encountered: