Conversation
|
Do you plan to continue @nemanja-tosic? |
Hello. Yes, i am planning on adding plugin dependency type support next hopefully some time next week. I do apologize for radio silence since i created the draft... |
|
Thanks for your work. |
Hi @jean-michelet. I pushed a change that demos the plugin dependency concept. Where i could use a bit of help is first of does it makes sense the way i have it and i could use help with implementation on the fastify side. It is necessary to pass plugins as object in order to extract types. So far, the same was achieved by passing strings for plugin names, so that will be a change. The createPlugin utility i have needs to be implemented in fastify-plugin, in a way that makes this interface possible. I will continue with updating to latest fastify and looking into the fastify-plugin side of things. Thanks! |
|
Looking into it. |
There was a problem hiding this comment.
The typing is a bit hard to read so I will have to get into it more seriously.
I love the idea of plugin dependencies, I don't know if we should upgrade fastifyPlugin or create a new utility createPlugin like you did.
2 worries:
- maybe I'm wrong, but I think you break a lot of stuff right now, I left a couple of comments.
- it requires a lot of work to use builder api on existing core plugins like you did, don't know if it's even doable:
const plugin1 = createPlugin((instance) =>
instance
.decorateReply('testPropSync')
.decorateReply('testValueSync', 'testValue')
.decorateReply('testFnSync', () => 12345))But I am ok to work on updating the core plugins if doable, we need more review on this PR.
| export const pluginWithDependencies = createPlugin((instance) => { | ||
| expectType<void>(instance.testPropSync) | ||
| expectType<string>(instance.testValueSync) | ||
| expectType<number>(instance.testFnSync()) | ||
|
|
||
| return instance.get('/', (req, res) => { | ||
| expectType<void>(req.testPropSync) | ||
| expectType<string>(req.testValueSync) | ||
| expectType<number>(req.testFnSync()) | ||
|
|
||
| expectType<void>(res.testPropSync) | ||
| expectType<string>(res.testValueSync) | ||
| expectType<number>(res.testFnSync()) | ||
| }) | ||
| }, { dependencies: [plugin1, plugin2, plugin3] }) |
There was a problem hiding this comment.
Instance inherit from parent scope, so I think we should add a test for it:
instance.register(childInstance => {
expectType<void>(childInstance.testPropSync)
expectType<string>(childInstance.testValueSync)
expectType<number>(childInstance.testFnSync())
})There was a problem hiding this comment.
I have added a test case, thank you for pointing this out.
| export type FastifyPluginCallback< | ||
| Options extends FastifyPluginOptions = Record<never, never>, | ||
| Server extends RawServerBase = RawServerDefault, | ||
| TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault, | ||
| Logger extends FastifyBaseLogger = FastifyBaseLogger | ||
| > = ( | ||
| instance: FastifyInstance<Server, RawRequestDefaultExpression<Server>, RawReplyDefaultExpression<Server>, Logger, TypeProvider>, | ||
| opts: Options, | ||
| done: (err?: Error) => void | ||
| ) => void | ||
| Options extends FastifyPluginOptions = FastifyPluginOptions, | ||
| TIn extends AnyFastifyInstance = AnyFastifyInstance, | ||
| TOut extends void | AnyFastifyInstance = void | AnyFastifyInstance | ||
| > = (instance: TIn, opts: Options, done: (err?: Error) => void) => TOut |
There was a problem hiding this comment.
Are you sure it's ok to break things this way? Don't you think it could surprise users?
There was a problem hiding this comment.
The question is difficult to answer. What do you mean by "break things this way"?
There was a problem hiding this comment.
Hi folks! 👋🏼😊 I agree with @jean-michelet, and we should be super careful with such an update, since it looks like a breaking change to me. Considering existing code like:
import type {
FastifyBaseLogger,
FastifyInstance,
RawReplyDefaultExpression,
RawRequestDefaultExpression,
RawServerDefault,
} from 'fastify';
import type { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts';
const dateSchema = {
$id: 'date',
type: 'string',
format: 'date',
} as const;
const jsonReferences = [
...dateSchema,
// all of my JSON Open API schema references ...
]
export type JsonTypeProvider = JsonSchemaToTsProvider<{ references: typeof [] }>;
export type FastifyJsonSchemaInstance = FastifyInstance<
RawServerDefault,
RawRequestDefaultExpression<RawServerDefault>,
RawReplyDefaultExpression<RawServerDefault>,
FastifyBaseLogger,
JsonTypeProvider
>;This will break the TS build 🤷🏼
There was a problem hiding this comment.
Thanks for the review. Can you please provide a full example that i can work against. I have not touched @fastify/type-provider-json-schema-to-ts so i don't know what's in play here.
Thanks
There was a problem hiding this comment.
You can refer to the code snippet above as a test for the change. By applying the updates in this PR, the TS build will fail. With the current fastify version, it build passes 😉
There was a problem hiding this comment.
Alright, i'll set up a reproduction. What error are you seeing, for reference?
There was a problem hiding this comment.
It's complicated to reproduce it here, I replicated it in a large codebase.
In general, the FastifyJsonSchemaInstance, that converts a JSON schema to types, no longer works, so the entire project (that relies on it), has the wrong types when accessing the JSON schemas
There was a problem hiding this comment.
You can basically check and compare what is returned (as type) by FastifyJsonSchemaInstance with this change, and what is returned with the current fastify version 😉 You'll see they differ drastically
There was a problem hiding this comment.
Applying the new version of fastify to fastify-type-provider-json-schema-to-ts does cause type tests to fail in that project, due to the difference in FastifyPluginCallback type params. The type declaration
export type FastifyPluginCallbackJsonSchemaToTs<
Options extends FastifyPluginOptions = Record<never, never>,
Server extends RawServerBase = RawServerDefault,
Logger extends FastifyBaseLogger = FastifyBaseLogger,
> = FastifyPluginCallback<Options, Server, JsonSchemaToTsProvider, Logger>;
was in general part of the scope that was supposed to be changed since we wanted to remove module augmentation. I will take a look if an interim solution is possible where FastifyPluginCallback can be preserved for this type of scenario.
| RequestType | ||
| > | ||
|
|
||
| export interface BaseFastifyRequest< |
There was a problem hiding this comment.
FastifyRequest might be imported and customized in many code bases... Are we sure this is a good idea to modify it? Shouldn't we create a new Type for the problem we are solving?
There was a problem hiding this comment.
FastifyRequest has the same interface, when looked from the outside. It was converted the way it was to incorporate Decorators. Previously, FastifyRequest was an interface, and i couldn't add a type from the generics to the interface. So a straightforward refactor was to introduce an intersection type.
From the outside, FastifyRequest shouldn't have changed when it came to properties it had, it should have only gotten a new prop. The change was meant to be backwards compatible. Do you have an example of breakage?
There was a problem hiding this comment.
The changes are meant to be backwards compatible to a reasonable degree. There will be some breaking changes, but i'm trying to keep the number down to a minimum. Doing what needs to be done here has certain requirements when it comes to type definitions. I don't see how to be 100% backwards compatible given the fact that previous code could not support what is needed.
The only way i could thing of to gain compile time safety with TypeScript in this scenario is to use return values to calculate the impact of a certain operation. If a method returns a decorated instance, we can infer things from the return type. If a method returns void, there is little that can be done in the context of compile time checks. It is possible to do plenty in run time, by asserting things on the fastify instance, but that was not the goal of these changes. Therefore, the switch to utilizing return values is a must the way i see it. Plugins can opt into this by either returning an instance or returning void in which case we cannot infer what the plugin provides, but things are backwards compatible. It is always possible to create a type definition that specifies what a plugin provides, but unless a plugin returns a value it is not possible to check whether the plugin does what the value says it should do, in compile time. |
It's probably the more reasonable for now, if you can provide a working example with FastifyStatic, it's great 👍 |
Needed for fastify-static poc.
Fastify static has a draft PR with the POC. I added the dependencies as well. There are FIXMEs which i am working through. First point of interest is the declaration of a plugin. The change is to types only, and the only aim is to provide a valid type definition that contains the decorators. Fastify types should handle properly merging the out instance Second point of interest is dependencies The code in fastify is focused on making the calls chainable in a way that provides inference so that you don't have to manually type. Core plugins can be manually annotated, like fastify-static was in this case. When manually annotating the plugin needs to make sure what it returns matches the definition on it's own. When relying on inference, the output must be correct as it's infered. Let me know what you think! |
Also fixes case where options weren't matching when explicitly definining plugin type.
Looks great! When do you think your work will be ready? |
I think there is an interest, one of the maintainer shared me your PR, but I don't know if there is a lot of contributors that have advanced knowledges of TypeScript. For me it's a bit hard to read for now and you understand better than me what to do, I think @climba03003 can share a valuable feedback, maybe when your PR will be ready for review. Anyway, keeping a close eye on it. |
|
Can we support this? export const deepPluginDependency = createPlugin((instance) =>
instance
.decorateReply('deep_dependency_reply', true))
export const pluginDependecy = createPlugin((instance) => instance
.decorateRequest('dependency_request', true), { dependencies: [deepPluginDependency] })
export const dependantPlugin = createPlugin((instance) => {
return instance.get('/', (req, res) => {
expectType<boolean>(req.dependency_request)
expectType<boolean>(res.deep_dependency_reply)
})
}, { dependencies: [pluginDependecy] }) |
|
I was thinking about this scenario, and i think that we should. I will see what needs to be done to get there. |
|
I had tried in the past and I remember the compiler thrown this error:
I think it's possible to ignore it or set an arbitrary depth level. |
|
Added support for transitive dependencies. |
|
@jean-michelet could you give me a hand with completing the fastify-plugin side of things please? At this moment i have the following definition which i used throughout the unit tests which currently passes all the tests. I could use a hand on the implementation side of things. I am also quite torn on whether this should be provided alongside fastify core, because i needed it most of the time during testing. Do you have an opinion on that? Thanks! |
We're trying to move away from these but they don't have to be removed.
Using the instance method because it is easier to infer types.
|
I didn't see that fastify-plugin was a dev dependency of fastify. I took advantage of that and create a new createPlugin function in fastify-plugin. We can either use a new function or change the old one. I'll weigh the options. I believe unless anyone can come up with further things we can tests i've reached a point where i'd say it could be reviewed. What remains then is to figure out the final implementation details in fastify-plugin. |
|
I'll take a look this week, thanks again for your work! Plz @climba03003, can you take a look took too, it looks like an interesting PR. |
rozzilla
left a comment
There was a problem hiding this comment.
This update contains important breaking changes. For instance, the number (and type) of the generics for the following types are updated:
- FastifyPluginCallback
- FastifyPluginAsync
- FastifyPlugin
- FastifyReply (
Decoratorsgeneric is added at the current position ofReplyTypegeneric) - FastifyRequest (as above)
If we want to proceed that way, I think we should mark it as a breaking change (so it should be included in v 6.x). Anyway, if possible, it'd be better to update this code so that we can keep TypeScript compatibility in the current version, so that we can ship it as it is.
| export type FastifyPluginCallback< | ||
| Options extends FastifyPluginOptions = Record<never, never>, | ||
| Server extends RawServerBase = RawServerDefault, | ||
| TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault, | ||
| Logger extends FastifyBaseLogger = FastifyBaseLogger | ||
| > = ( | ||
| instance: FastifyInstance<Server, RawRequestDefaultExpression<Server>, RawReplyDefaultExpression<Server>, Logger, TypeProvider>, | ||
| opts: Options, | ||
| done: (err?: Error) => void | ||
| ) => void | ||
| Options extends FastifyPluginOptions = FastifyPluginOptions, | ||
| TIn extends AnyFastifyInstance = AnyFastifyInstance, | ||
| TOut extends void | AnyFastifyInstance = void | AnyFastifyInstance | ||
| > = (instance: TIn, opts: Options, done: (err?: Error) => void) => TOut |
There was a problem hiding this comment.
Hi folks! 👋🏼😊 I agree with @jean-michelet, and we should be super careful with such an update, since it looks like a breaking change to me. Considering existing code like:
import type {
FastifyBaseLogger,
FastifyInstance,
RawReplyDefaultExpression,
RawRequestDefaultExpression,
RawServerDefault,
} from 'fastify';
import type { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts';
const dateSchema = {
$id: 'date',
type: 'string',
format: 'date',
} as const;
const jsonReferences = [
...dateSchema,
// all of my JSON Open API schema references ...
]
export type JsonTypeProvider = JsonSchemaToTsProvider<{ references: typeof [] }>;
export type FastifyJsonSchemaInstance = FastifyInstance<
RawServerDefault,
RawRequestDefaultExpression<RawServerDefault>,
RawReplyDefaultExpression<RawServerDefault>,
FastifyBaseLogger,
JsonTypeProvider
>;This will break the TS build 🤷🏼
There is a comment for FastifyRequest for RequestType, i assumed the same applies for FastifyReply. Granted the same comment isn't present, but it looks to be the same case. EDIT: that said, decorators CAN be last, since i don't expect the to be explicitly passed. I only did it this way to preserve what i thought was wanted. |
|
I think we can assume we'll ship this in v6 unless it could be made less breaking. This will give us time to go update all the plugins too. @nemanja-tosic could you add a little bit of a migration guide? Thanks |
|
I'm not seeing the comments i have on a reviewed file so i will summarize here. The concern for PluginCallback is valid due to the type change, since the type is used to declare plugins and forcing a migration immediately might be difficult. I will take a look if it is possible to support the previous version alongside the new one using https://github.com/fastify/fastify-type-provider-json-schema-to-ts as an example. Acceptance criteria would be that
Based on that i will know what is needed for migration. Based on the reviews, FastifyPluginCallback, FastifyPluginCallbackAsync and FastifyPlugin are the main concerns for the work so far, so i will attempt to make those backwards compatible. Thank you for the review @rozzilla |
Yup, exactly! By moving I would also add another criteria we should consider:
If we need to update an existing type test, this means the change is not backwards compatible |
|
tsd cannot remain as they are today due to the differences between expectType and expectAssignable. The changes update the instance type in order to enforce type safety, so tests will likely have to follow suite. |
|
@jean-michelet could you take another look at this PR? |
|
I don't feel confident/competent enough to give more feedback than I've already given. In short :
|
|
Closing as too old |
WIP #5093
As part of the work to update types, we have the following changes in addition to this PR:
The work in this PR provides support for inferring type definitions after a plugin is registered. The idea is to capture the decorators added to request, reply and instance based on the return type.
This behavior is opt in - it is possible to define plugins without returning the resulting instance (or returning a partially inferred instance) which makes the change backwards compatible, but then TypeScript cannot infer the resulting instance and so manual typing is needed. Given examples from the community, we can provide a migration path and improve the approach.
The ultimate goal is compile time safety, so in order to gain that something needs to be traded off.
The work in fastify-static shows how a core plugin can be updated in a way where we know the decorators it provides. The change is limited to type definitions, and so makes it backwards compatible as well.
fastify-plugin should provide an implementation for a plugin factory that results in un-encapsulated plugins. A new function will be added, which makes the change backwards compatible.
The final API can be made according to feedback from the community - whether createPlugin is part of core etc, i am very happy to make changes given feedback.
Checklist
npm run testandnpm run benchmarkand the Code of conduct