-
Notifications
You must be signed in to change notification settings - Fork 10.3k
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
fix(gatsby): update plugin api types #30819
Conversation
Hi!
Yep, sounds correct 👍 People can use any of those three but don't have to
Via Git Blame I traced it back to #10897 and I think at that point in time we just accepted it as is, but I also don't see how they could be async
Yep, had similar experiences in the past
Can you expand on that a bit, maybe with a code example? For now when I have my custom plugin options I create an interface that extends Gatsby's
👍
Porting such a huge codebase over to TS takes time and effort -- we really want that file to be autogenerated but we only got so many people to handle everything here 😅
If you want you can take this to an RFC: https://github.com/gatsbyjs/gatsby/discussions/categories/rfc |
Absolutely! The method that could be used now would be declaration merging, but I only left it that way because I was trying to minimize the changes to the public types. It's a little bit esoteric and isn't very common for public APIs, but can't say it's never used... it may also cause some additional problems I mentioned when importing other people's plugins: // foo-plugin/gatsby-node
import { GatsbyNode, PluginOptions } from "gatsby";
declare module "gatsby" {
interface PluginOptions {
foo: string;
}
}
export const sourceNodes: GatsbyNode["sourceNodes"] = (args, options) => {
const test: string = options.foo; // No errors
} The reason I say there may be an issue with importing that plugin is if // bar-plugin/gatsby-node
// Let's say `someUtility` is getting its types from the same module where the declaration merging was done
import { someUtility } from "foo-plugin";
import { PluginOptions, GatsbyNode } from "gatsby";
interface IMyPluginOptions extends PluginOptions {
bar: string;
}
export const sourceNodes: GatsbyNode["sourceNodes"] = (args, options: IMyPluginOptions) => {
const bar: string = options.bar; // Expected to be good
// With `noUncheckedIndexedAccess`, I would expect an error here; but with the declaration
// merging, this plugin might inherit the types of `foo-plugin`
const test: string = options.foo;
} I can't remember if Typescript will ignore declaration merging done in 3rd party libraries. Would need to test it. Probably a better method (and arguably more common) would be to define the extended plugin options as generic on // With generic parameter. This has the added benefit of making them strictly typed when TExtendOptions is defined
export type PluginOptions<
TExtendOptions extends Record<string, unknown> = Record<string, unknown>
> = {
plugins: unknown[]
} & TExtendOptions;
export interface GatsbyNode<
TPluginOptions extends Record<string, unknown> = Record<string, unknown>,
TNode extends Record<string, unknown> = Record<string, unknown>,
TContext = Record<string, unknown>
> {
sourceNodes?(
args: SourceNodesArgs,
options: PluginOptions<TPluginOptions>, // IMO this is cleaner than using unions everywhere
callback: PluginCallback<void>
): void | Promise<void>
}
// With a union instead
export interface PluginOptions {
plugins: unknown[]
[key: string]: unknown
}
export interface GatsbyNode<
TPluginOptions extends Record<string, unknown> = Record<string, unknown>,
TNode extends Record<string, unknown> = Record<string, unknown>,
TContext = Record<string, unknown>
> {
sourceNodes?(
args: SourceNodesArgs,
options: PluginOptions & TPluginOptions, // This would have to be done everywhere `PluginOptions` is used
callback: PluginCallback<void>
): void | Promise<void>
} Usage is the same with both definitions: // Some plugin
import { GatsbyNode } from "gatsby";
interface IPluginOptions {
foo: string;
}
// I'd probably create an alias
type MyGatsbyNode = GatsbyNode<IPluginOptions>;
/* Maybe if I wanted to allow it to define different kinds of nodes: */
// type MyGatsbyNode<TNode extends Record<string, unknown> = Record<string, unknown> = (
// GatsbyNode<IPluginOptions, TNode>
// );
const sourceNodes: MyGatsbyNode["sourceNodes"] = (args, options) => {
const test: string = options.foo; // Should pass
} |
I was tempted to fix all the linting issues, but I have run into some small problems when converting things like That would also make this PR a lot more difficult to read, because the number of changes would be... a lot 😆 I struggle to see how much of this can be autogenerated. I guess ideally there would be an interface where things like
Sure, I can do that probably this weekend. It would be so cool to get away from the magic exports, but once a project has come this far it's a struggle to change the public API so drastically. Wind up with this compatibility layer to support both for a while, and then all that extra work to strip that layer out after the old style has been successfully deprecated, not to mention all the extra maintenance for both and the in-between 😞. I've been considering making this type of usage an option in |
Thanks for the explanations on these things! I'll get this merged as-is (and then it'll be released 11th of May) and would still be really interested in a RFC write-up. We'll want to introduce a Strict Mode into Gatsby, maybe parts of the ideas could flow into this then. |
* master: (45 commits) chore(release): Publish next pre-minor fix(gatsby-source-shopify): fix linting (gatsbyjs#31291) fix(deps): update minor and patch for gatsby-plugin-preact (gatsbyjs#31169) chore: add gatsby-plugin-gatsby-cloud to renovate chore: update renovatebot config to support more packages (gatsbyjs#31289) chore(deps): update dependency @types/semver to ^7.3.5 (gatsbyjs#31148) fix(deps): update minor and patch for gatsby-plugin-manifest (gatsbyjs#31160) fix(deps): update minor and patch for gatsby-remark-copy-linked-files (gatsbyjs#31163) fix(deps): update dependency mini-css-extract-plugin to v1.6.0 (gatsbyjs#31158) chore(deps): update dependency @testing-library/react to ^11.2.6 (gatsbyjs#31168) docs(gatsby-source-shopify): Updates Shopify README with new plugin info (gatsbyjs#31287) chore: run yarn deduplicate (gatsbyjs#31285) docs(gatsby-plugin-image): Add docs for customizing default options (gatsbyjs#30344) fix(gatsby-plugin-image): print error details (gatsbyjs#30417) chore(docs): Update "Adding Search with Algolia" guide (gatsbyjs#29460) chore(docs): Update MDX frontmatter for programmatic pages (gatsbyjs#29798) docs: Add image plugin architecture doc (gatsbyjs#31096) perf(gatsby): use fastq instead of better-queue + refactor (gatsbyjs#31269) feat(gatsby-plugin-image): Export ImageDataLike type (gatsbyjs#30590) fix(gatsby): update plugin api types (gatsbyjs#30819) ...
Co-authored-by: gatsbybot <mathews.kyle+gatsbybot@gmail.com>
Description
This updates the public types exposed for plugin api endpoints so that Typescript is better able to resolve types of function signatures.
I opted for simplicity, removing the various function overloads that were causing issues.
I've also fixed the return types for certain endpoints, updated the signature for some endpoints that didn't seem accurate (
GatsbySSR
), and added some generic parameters that will allow more effective usage of some of the endpoints (onCreateNode
,onCreatePage
, etc).Each of the
GatsbyNode
apis can be characterized with all three parameters:args
,options
, and thecallback
. Whoever is writing the endpoint can choose which parameters they wish to consume.Go to definition
and walk up the type definitions to see what the actual function signature is.In
GatsbySSR
, it appears as though they do not receive acallback
parameter. Not only that, but I don't believe any of them can be asynchronous? Please correct me if I'm wrongThere were several types that take generic parameters, but the way the types were designed made it impossible to actually use those generics. Anytime I've wanted to, I've had to recreate the function types myself. This adds those parameters to the interface (instead of on the property) and passes them down to the types from there.
I also added a generic on
createNode
, so that you can make sure the nodes you're creating are strictly typed.By doing this, I've changedInputNode
to atype
instead of aninterface
. This means that declaration merging won't have any affect on it now. I thought it was pretty safe to do, but perhaps there's people out there that have used declaration merging to type their nodes? It seems like it would be a strange thing to do for me, and it would be simple enough to switch over to using the generic parameter instead.createNode()
I thought about adding a generic to
PluginOptions
too, so that plugin designers could make sure their plugin options are strongly typed. Since the types have been broken for some time, I've usually just recreated the function signatures in my projects, with their own strongly typed plugin options.PluginOptions
. I can't remember if third-party libraries will affect your own definitions if you import their module. For example, if I importfoo
library to use some function they've created, and they used declaration merging to define theirPluginOptions
, will it affect my ownPluginOptions
.The return types for various endpoints were rather ambiguous, when they really could have been more explicit.
Additional Context
There is a lot of eslint issues in
index.d.ts
. I'm not sure how much this matters... I had to fight my editor every step of the way in order to keep eslint turned on (I prefer using auto-fix, and don't like adjusting my settings for individual files) 😆. It also makes it harder to find legitimate issuesIf we're being honest, the plugin api design makes it a little bit cumbersome to type & consume those types. I think it would be awesome to see a "plugin builder" api instead, which could be used like this:
This type of plugin creator structure makes types so much clearer and easier to work with. If it were to accept both an array of builders or the single builder method itself, it would also be really simple to break up your
gatsby-node
into multiple modules; just import them all into thegatsby-node
and expose them as an array of function references.Just some thoughts 🤷♂️
Related Issues
Closes #23296