fix(serve-static): make response generic#48591
Conversation
|
@devanshj Thank you for submitting this PR! This is a live comment which I will keep updated. 1 package in this PRCode ReviewsBecause this is a widely-used package, a DT maintainer will need to review it before it can be merged. Status
Once every item on this list is checked, I'll ask you for permission to merge and publish the changes. Diagnostic Information: What the bot saw about this PR{
"type": "info",
"now": "-",
"pr_number": 48591,
"author": "devanshj",
"owners": [
"urossmolnik",
"LinusU"
],
"dangerLevel": "ScopedAndTested",
"headCommitAbbrOid": "fbb12c7",
"headCommitOid": "fbb12c7e0c34275dbea1a4bdb62dc50fd24b0dd3",
"mergeIsRequested": false,
"stalenessInDays": 2,
"lastPushDate": "2020-10-10T15:47:21.000Z",
"lastCommentDate": "2020-10-16T17:01:59.000Z",
"maintainerBlessed": false,
"reviewLink": "https://github.com/DefinitelyTyped/DefinitelyTyped/pull/48591/files",
"hasMergeConflict": false,
"authorIsOwner": false,
"isFirstContribution": false,
"popularityLevel": "Critical",
"newPackages": [],
"packages": [
"serve-static"
],
"files": [
{
"path": "types/serve-static/index.d.ts",
"kind": "definition",
"package": "serve-static"
},
{
"path": "types/serve-static/serve-static-tests.ts",
"kind": "test",
"package": "serve-static"
}
],
"hasDismissedReview": false,
"ciResult": "pass",
"reviewersWithStaleReviews": [
{
"reviewedAbbrOid": "dcb021a",
"reviewer": "rbuckton",
"date": "2020-10-10T00:53:22Z"
}
],
"approvalFlags": 0,
"isChangesRequested": false
} |
|
🔔 @urossmolnik @LinusU — please review this PR in the next few days. Be sure to explicitly select |
| interface RequestHandler<R extends http.OutgoingMessage> { | ||
| (request: http.IncomingMessage, response: R, next: () => void): any; | ||
| } | ||
| function serveStatic(root: string, options?: ServeStaticOptions): express.Handler; |
There was a problem hiding this comment.
I'm not sure if this is required or what, seems it's not so I removed it as it looks like a duplicate declaration.
There was a problem hiding this comment.
I think it may have been added to (incorrectly) model --esModuleInterop. serveStatic doesn't assign itself as a property of itself, so removing this is correct.
There was a problem hiding this comment.
Yeah exactly I realized that later and was about to point that out as I myself had hit a runtime error because import { serveStatic } from "serve-static" compiled anyway.
| declare namespace serveStatic { | ||
| var mime: typeof m; | ||
| interface ServeStaticOptions { | ||
| interface ServeStaticOptions<R extends http.OutgoingMessage = http.OutgoingMessage> { |
There was a problem hiding this comment.
Making the type parameter optional to avoid unnecessary breaking change for those who have used ServeStaticOptions in their codebase. I often avoid going so overboard to avoid breaking changes in types but it's not that bad hence went with it.
There was a problem hiding this comment.
Isn't this still a breaking change? Anyone who had previously used serveStatic with a custom setHeaders callback will now get the weaker http.OutgoingMessage instead of express.Response when not contextually typed. This is mitigated somewhat by the fact that app.use(serveStatic(...)) should result in R being contextually typed to express.Response, but if anyone writes code like this then they will be broken:
const middleware = serveStatic(...); // no contextual type
app.use(middleware); // error: `OutgoingMessage` is not assignable to `Response`.It would be good to have the tests verify the contextual type for the app.use(serveStatic(...)) case. I'm not sure how often anyone writes serveStatic(...) outside of a .use(), so I'm not sure how much of an impact this change will be.
There was a problem hiding this comment.
Well ultimately it is a breaking change although making type parameter optional saves some error in cases where users have written const options: ServeStaticOptions = { ... } and are not using express exclusive methods on the response.
[...] but if anyone writes code like this then they will be broken:
const middleware = serveStatic(...); // no contextual type app.use(middleware); // error: `OutgoingMessage` is not assignable to `Response`.
Umm, not really? This would compile ...
const middleware = serveStatic({ setHeaders: res => res.setHeader("foo, bar") }) // R is http.OutgoingMessage
app.use(middleware) // works because `use` will invoke with `express.Response` which is wider than `http.OutgoingMessage` which is required by `middleware`... however this would not (which would have compiled with previous version) ...
const middleware = serveStatic({ setHeaders: res => res.set("foo, bar") }) // R is http.OutgoingMessage, error: property set does not exist
app.use(middleware) // no error hereIt would be good to have the tests verify the contextual type for the app.use(serveStatic(...)) case.
Yes as you might have seen I have added tests for this and the above expected error as well.
I'm not sure how often anyone writes serveStatic(...) outside of a .use(), so I'm not sure how much of an impact this change will be.
Yeah and even if they do write serveStatic(...) outside use it's won't be a breaking change as long as they don't use express exclusive methods in setHeaders. As I had said in the description:
So it indeed is a breaking change but only for people who invoke the serveStatic with setHeaders option using express apis and the invocation is not directly passed to use (which makes the response not infer to express.Response).
But all things said I don't think this will have a huge impact.
|
|
||
| import * as express from "express-serve-static-core"; | ||
| import * as m from "mime"; | ||
| import * as http from "http"; |
There was a problem hiding this comment.
This assumes users have @types/node installed, which I guess is okay?
There was a problem hiding this comment.
You should probably indicate that with /// <reference types="node" />.
| maxAge: 0, | ||
| redirect: true, | ||
| setHeaders: function(res: express.Response, path: string, stat: any) { | ||
| setHeaders: function(res, path: string, stat: any) { |
There was a problem hiding this comment.
(does work with : express.Response too ofc)
There was a problem hiding this comment.
Please add an // $ExpectType on the next line to verify the contextual type is still express.Response.
|
👋 Hi there! I’ve run some quick measurements against master and your PR. These metrics should help the humans reviewing this PR gauge whether it might negatively affect compile times or editor responsiveness for users who install these typings. Let’s review the numbers, shall we? Comparison details 📊
It looks like nothing changed too much. I won’t post performance data again unless it gets worse. |
|
|
||
| import * as express from "express-serve-static-core"; | ||
| import * as m from "mime"; | ||
| import * as http from "http"; |
There was a problem hiding this comment.
You should probably indicate that with /// <reference types="node" />.
| maxAge: 0, | ||
| redirect: true, | ||
| setHeaders: function(res: express.Response, path: string, stat: any) { | ||
| setHeaders: function(res, path: string, stat: any) { |
There was a problem hiding this comment.
Please add an // $ExpectType on the next line to verify the contextual type is still express.Response.
|
|
||
| app.use(serveStatic('/infers-express-response-when-passed-to-express-use', { | ||
| setHeaders: function(res) { | ||
| res.set('foo', 'bar'); |
There was a problem hiding this comment.
Please add an // $ExpectType on the next line to verify the contextual type is still express.Response.
| declare namespace serveStatic { | ||
| var mime: typeof m; | ||
| interface ServeStaticOptions { | ||
| interface ServeStaticOptions<R extends http.OutgoingMessage = http.OutgoingMessage> { |
There was a problem hiding this comment.
Isn't this still a breaking change? Anyone who had previously used serveStatic with a custom setHeaders callback will now get the weaker http.OutgoingMessage instead of express.Response when not contextually typed. This is mitigated somewhat by the fact that app.use(serveStatic(...)) should result in R being contextually typed to express.Response, but if anyone writes code like this then they will be broken:
const middleware = serveStatic(...); // no contextual type
app.use(middleware); // error: `OutgoingMessage` is not assignable to `Response`.It would be good to have the tests verify the contextual type for the app.use(serveStatic(...)) case. I'm not sure how often anyone writes serveStatic(...) outside of a .use(), so I'm not sure how much of an impact this change will be.
|
@devanshj One or more reviewers has requested changes. Please address their comments. I'll be back once they sign off or you've pushed new commits or comments. If you disagree with the reviewer's comments, you can "dismiss" the review using GitHub's review UI. Thank you! |
|
@devanshj The CI build failed! Please review the logs for more information. Once you've pushed the fixes, the build will automatically re-run. Thanks! |
cabf2b7 to
3c5457f
Compare
|
@devanshj The CI build failed! Please review the logs for more information. Once you've pushed the fixes, the build will automatically re-run. Thanks! |
3c5457f to
fbb12c7
Compare
|
|
||
| app.use(serveStatic('/infers-express-response-when-passed-to-express-use', { | ||
| setHeaders: function(res) { | ||
| // $ExpectType Response<any, number> |
There was a problem hiding this comment.
Not sure why but express.Response doesn't work, nor does express.Response<any, number> work. dtslint complains it needs to be Response<any, number>, so going with it.
|
@rbuckton Thank you for reviewing this PR! The author has pushed new commits since your last review. Could you take another look and submit a fresh review? |
|
Ping 🔔 @urossmolnik @LinusU - I'd like to get one more opinion on this PR from someone with experience using the library to make the trade-offs for the breaking change in the comments. |
|
Fwiw there isn't an option other than breaking it because the types were written incorrectly originally (not picking on anyone just objectively speaking). |
|
Sorry haven't used this package in a while. |
|
👍🏻 |
|
I just published |
|
This breaks the tests for |
Yeah the fix would be to annotate |
Right now the types are as if the library is coupled to express, which is not the case as the readme shows an example of it running with node server and nowhere in the source code it uses express-specific methods on the response or request.
So because the types of
requestandresponseare that of express (which is not needed) the example from the readme doesn't compile. So to fix it, I've made the types adhere to the implementation ie the request and response can be anything as long as they extendhttp.IncomingMessageandhttp.OutgoingMessagerespectively. Response needs to be generic as it's used insetHeadersoption.So it indeed is a breaking change but only for people who invoke the
serveStaticwithsetHeadersoption using express apis and the invocation is not directly passed touse(which makes the response not infer toexpress.Response). The first commit adds test case to indicate breaking change.Use a meaningful title for the pull request. Include the name of the package modified.
Test the change in your own code. (Compile and run.)
Add or edit tests to reflect the change. (Run with
npm test YOUR_PACKAGE_NAME.)Follow the advice from the readme.
Avoid common mistakes.
Run
npm run lint package-name(ortscif notslint.jsonis present).Provide a URL to documentation or source code which provides context for the suggested changes: https://github.com/expressjs/serve-static#serve-files-with-vanilla-nodejs-http-server
If this PR brings the type definitions up to date with a new version of the JS library, update the version number in the header.
Include tests for your changes
If you are making substantial changes, consider adding a
tslint.jsoncontaining{ "extends": "dtslint/dt.json" }. If for reason the any rule need to be disabled, disable it for that line using// tslint:disable-next-line [ruleName]and not for whole package so that the need for disabling can be reviewed.