From 695f972216e19e93ab11bf6b1dfc9fd1f08213fb Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Sun, 9 Mar 2025 10:46:29 +0100 Subject: [PATCH 01/16] Moving types into the repo incl. their validation. --- core.d.ts | 1244 ++++++++++++++++++++++++++++++++++++++++++++++++++ index.d.ts | 131 ++++++ package.json | 7 +- 3 files changed, 1381 insertions(+), 1 deletion(-) create mode 100644 core.d.ts create mode 100644 index.d.ts diff --git a/core.d.ts b/core.d.ts new file mode 100644 index 00000000000..9e31f573a63 --- /dev/null +++ b/core.d.ts @@ -0,0 +1,1244 @@ +// This extracts the core definitions from express to prevent a circular dependency between express and serve-static +/// + +import { SendOptions } from "send"; + +declare global { + namespace Express { + // These open interfaces may be extended in an application-specific manner via declaration merging. + // See for example method-override.d.ts (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/method-override/index.d.ts) + interface Request {} + interface Response {} + interface Locals {} + interface Application {} + } +} + +import { EventEmitter } from "events"; +import * as http from "http"; +import { ParsedQs } from "qs"; +import { Options as RangeParserOptions, Ranges as RangeParserRanges, Result as RangeParserResult } from "range-parser"; + +export {}; + +export type Query = ParsedQs; + +export interface NextFunction { + (err?: any): void; + /** + * "Break-out" of a router by calling {next('router')}; + * @see {https://expressjs.com/en/guide/using-middleware.html#middleware.router} + */ + (deferToNext: "router"): void; + /** + * "Break-out" of a route by calling {next('route')}; + * @see {https://expressjs.com/en/guide/using-middleware.html#middleware.application} + */ + (deferToNext: "route"): void; +} + +export interface Dictionary { + [key: string]: T; +} + +export interface ParamsDictionary { + [key: string]: string; +} +export type ParamsArray = string[]; +export type Params = ParamsDictionary | ParamsArray; + +export interface Locals extends Express.Locals {} + +export interface RequestHandler< + P = ParamsDictionary, + ResBody = any, + ReqBody = any, + ReqQuery = ParsedQs, + LocalsObj extends Record = Record, +> { + // tslint:disable-next-line callable-types (This is extended from and can't extend from a type alias in ts<2.2) + ( + req: Request, + res: Response, + next: NextFunction, + ): void | Promise; +} + +export type ErrorRequestHandler< + P = ParamsDictionary, + ResBody = any, + ReqBody = any, + ReqQuery = ParsedQs, + LocalsObj extends Record = Record, +> = ( + err: any, + req: Request, + res: Response, + next: NextFunction, +) => void | Promise; + +export type PathParams = string | RegExp | Array; + +export type RequestHandlerParams< + P = ParamsDictionary, + ResBody = any, + ReqBody = any, + ReqQuery = ParsedQs, + LocalsObj extends Record = Record, +> = + | RequestHandler + | ErrorRequestHandler + | Array | ErrorRequestHandler

>; + +type RemoveTail = S extends `${infer P}${Tail}` ? P : S; +type GetRouteParameter = RemoveTail< + RemoveTail, `-${string}`>, + `.${string}` +>; + +// prettier-ignore +export type RouteParameters = Route extends `${infer Required}{${infer Optional}}${infer Next}` + ? ParseRouteParameters & Partial> & RouteParameters + : ParseRouteParameters; + +type ParseRouteParameters = string extends Route ? ParamsDictionary + : Route extends `${string}(${string}` ? ParamsDictionary // TODO: handling for regex parameters + : Route extends `${string}:${infer Rest}` ? + & ( + GetRouteParameter extends never ? ParamsDictionary + : GetRouteParameter extends `${infer ParamName}?` ? { [P in ParamName]?: string } // TODO: Remove old `?` handling when Express 5 is promoted to "latest" + : { [P in GetRouteParameter]: string } + ) + & (Rest extends `${GetRouteParameter}${infer Next}` ? RouteParameters : unknown) + : {}; + +/* eslint-disable @definitelytyped/no-unnecessary-generics */ +export interface IRouterMatcher< + T, + Method extends "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head" = any, +> { + < + Route extends string, + P = RouteParameters, + ResBody = any, + ReqBody = any, + ReqQuery = ParsedQs, + LocalsObj extends Record = Record, + >( + // (it's used as the default type parameter for P) + path: Route, + // (This generic is meant to be passed explicitly.) + ...handlers: Array> + ): T; + < + Path extends string, + P = RouteParameters, + ResBody = any, + ReqBody = any, + ReqQuery = ParsedQs, + LocalsObj extends Record = Record, + >( + // (it's used as the default type parameter for P) + path: Path, + // (This generic is meant to be passed explicitly.) + ...handlers: Array> + ): T; + < + P = ParamsDictionary, + ResBody = any, + ReqBody = any, + ReqQuery = ParsedQs, + LocalsObj extends Record = Record, + >( + path: PathParams, + // (This generic is meant to be passed explicitly.) + ...handlers: Array> + ): T; + < + P = ParamsDictionary, + ResBody = any, + ReqBody = any, + ReqQuery = ParsedQs, + LocalsObj extends Record = Record, + >( + path: PathParams, + // (This generic is meant to be passed explicitly.) + ...handlers: Array> + ): T; + (path: PathParams, subApplication: Application): T; +} + +export interface IRouterHandler { + (...handlers: Array>>): T; + (...handlers: Array>>): T; + < + P = RouteParameters, + ResBody = any, + ReqBody = any, + ReqQuery = ParsedQs, + LocalsObj extends Record = Record, + >( + // (This generic is meant to be passed explicitly.) + // eslint-disable-next-line @definitelytyped/no-unnecessary-generics + ...handlers: Array> + ): T; + < + P = RouteParameters, + ResBody = any, + ReqBody = any, + ReqQuery = ParsedQs, + LocalsObj extends Record = Record, + >( + // (This generic is meant to be passed explicitly.) + // eslint-disable-next-line @definitelytyped/no-unnecessary-generics + ...handlers: Array> + ): T; + < + P = ParamsDictionary, + ResBody = any, + ReqBody = any, + ReqQuery = ParsedQs, + LocalsObj extends Record = Record, + >( + // (This generic is meant to be passed explicitly.) + // eslint-disable-next-line @definitelytyped/no-unnecessary-generics + ...handlers: Array> + ): T; + < + P = ParamsDictionary, + ResBody = any, + ReqBody = any, + ReqQuery = ParsedQs, + LocalsObj extends Record = Record, + >( + // (This generic is meant to be passed explicitly.) + // eslint-disable-next-line @definitelytyped/no-unnecessary-generics + ...handlers: Array> + ): T; +} +/* eslint-enable @definitelytyped/no-unnecessary-generics */ + +export interface IRouter extends RequestHandler { + /** + * Map the given param placeholder `name`(s) to the given callback(s). + * + * Parameter mapping is used to provide pre-conditions to routes + * which use normalized placeholders. For example a _:user_id_ parameter + * could automatically load a user's information from the database without + * any additional code, + * + * The callback uses the samesignature as middleware, the only differencing + * being that the value of the placeholder is passed, in this case the _id_ + * of the user. Once the `next()` function is invoked, just like middleware + * it will continue on to execute the route, or subsequent parameter functions. + * + * app.param('user_id', function(req, res, next, id){ + * User.find(id, function(err, user){ + * if (err) { + * next(err); + * } else if (user) { + * req.user = user; + * next(); + * } else { + * next(new Error('failed to load user')); + * } + * }); + * }); + */ + param(name: string, handler: RequestParamHandler): this; + + /** + * Special-cased "all" method, applying the given route `path`, + * middleware, and callback to _every_ HTTP method. + */ + all: IRouterMatcher; + get: IRouterMatcher; + post: IRouterMatcher; + put: IRouterMatcher; + delete: IRouterMatcher; + patch: IRouterMatcher; + options: IRouterMatcher; + head: IRouterMatcher; + + checkout: IRouterMatcher; + connect: IRouterMatcher; + copy: IRouterMatcher; + lock: IRouterMatcher; + merge: IRouterMatcher; + mkactivity: IRouterMatcher; + mkcol: IRouterMatcher; + move: IRouterMatcher; + "m-search": IRouterMatcher; + notify: IRouterMatcher; + propfind: IRouterMatcher; + proppatch: IRouterMatcher; + purge: IRouterMatcher; + report: IRouterMatcher; + search: IRouterMatcher; + subscribe: IRouterMatcher; + trace: IRouterMatcher; + unlock: IRouterMatcher; + unsubscribe: IRouterMatcher; + link: IRouterMatcher; + unlink: IRouterMatcher; + + use: IRouterHandler & IRouterMatcher; + + route(prefix: T): IRoute; + route(prefix: PathParams): IRoute; + /** + * Stack of configured routes + */ + stack: ILayer[]; +} + +export interface ILayer { + route?: IRoute; + name: string | ""; + params?: Record; + keys: string[]; + path?: string; + method: string; + regexp: RegExp; + handle: (req: Request, res: Response, next: NextFunction) => any; +} + +export interface IRoute { + path: string; + stack: ILayer[]; + all: IRouterHandler; + get: IRouterHandler; + post: IRouterHandler; + put: IRouterHandler; + delete: IRouterHandler; + patch: IRouterHandler; + options: IRouterHandler; + head: IRouterHandler; + + checkout: IRouterHandler; + copy: IRouterHandler; + lock: IRouterHandler; + merge: IRouterHandler; + mkactivity: IRouterHandler; + mkcol: IRouterHandler; + move: IRouterHandler; + "m-search": IRouterHandler; + notify: IRouterHandler; + purge: IRouterHandler; + report: IRouterHandler; + search: IRouterHandler; + subscribe: IRouterHandler; + trace: IRouterHandler; + unlock: IRouterHandler; + unsubscribe: IRouterHandler; +} + +export interface Router extends IRouter {} + +/** + * Options passed down into `res.cookie` + * @link https://expressjs.com/en/api.html#res.cookie + */ +export interface CookieOptions { + /** Convenient option for setting the expiry time relative to the current time in **milliseconds**. */ + maxAge?: number | undefined; + /** Indicates if the cookie should be signed. */ + signed?: boolean | undefined; + /** Expiry date of the cookie in GMT. If not specified (undefined), creates a session cookie. */ + expires?: Date | undefined; + /** Flags the cookie to be accessible only by the web server. */ + httpOnly?: boolean | undefined; + /** Path for the cookie. Defaults to “/”. */ + path?: string | undefined; + /** Domain name for the cookie. Defaults to the domain name of the app. */ + domain?: string | undefined; + /** Marks the cookie to be used with HTTPS only. */ + secure?: boolean | undefined; + /** A synchronous function used for cookie value encoding. Defaults to encodeURIComponent. */ + encode?: ((val: string) => string) | undefined; + /** + * Value of the “SameSite” Set-Cookie attribute. + * @link https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00#section-4.1.1. + */ + sameSite?: boolean | "lax" | "strict" | "none" | undefined; + /** + * Value of the “Priority” Set-Cookie attribute. + * @link https://datatracker.ietf.org/doc/html/draft-west-cookie-priority-00#section-4.3 + */ + priority?: "low" | "medium" | "high"; + /** Marks the cookie to use partioned storage. */ + partitioned?: boolean | undefined; +} + +export interface ByteRange { + start: number; + end: number; +} + +export interface RequestRanges extends RangeParserRanges {} + +export type Errback = (err: Error) => void; + +/** + * @param P For most requests, this should be `ParamsDictionary`, but if you're + * using this in a route handler for a route that uses a `RegExp` or a wildcard + * `string` path (e.g. `'/user/*'`), then `req.params` will be an array, in + * which case you should use `ParamsArray` instead. + * + * @see https://expressjs.com/en/api.html#req.params + * + * @example + * app.get('/user/:id', (req, res) => res.send(req.params.id)); // implicitly `ParamsDictionary` + * app.get(/user\/(.*)/, (req, res) => res.send(req.params[0])); + * app.get('/user/*', (req, res) => res.send(req.params[0])); + */ +export interface Request< + P = ParamsDictionary, + ResBody = any, + ReqBody = any, + ReqQuery = ParsedQs, + LocalsObj extends Record = Record, +> extends http.IncomingMessage, Express.Request { + /** + * Return request header. + * + * The `Referrer` header field is special-cased, + * both `Referrer` and `Referer` are interchangeable. + * + * Examples: + * + * req.get('Content-Type'); + * // => "text/plain" + * + * req.get('content-type'); + * // => "text/plain" + * + * req.get('Something'); + * // => undefined + * + * Aliased as `req.header()`. + */ + get(name: "set-cookie"): string[] | undefined; + get(name: string): string | undefined; + + header(name: "set-cookie"): string[] | undefined; + header(name: string): string | undefined; + + /** + * Check if the given `type(s)` is acceptable, returning + * the best match when true, otherwise `undefined`, in which + * case you should respond with 406 "Not Acceptable". + * + * The `type` value may be a single mime type string + * such as "application/json", the extension name + * such as "json", a comma-delimted list such as "json, html, text/plain", + * or an array `["json", "html", "text/plain"]`. When a list + * or array is given the _best_ match, if any is returned. + * + * Examples: + * + * // Accept: text/html + * req.accepts('html'); + * // => "html" + * + * // Accept: text/*, application/json + * req.accepts('html'); + * // => "html" + * req.accepts('text/html'); + * // => "text/html" + * req.accepts('json, text'); + * // => "json" + * req.accepts('application/json'); + * // => "application/json" + * + * // Accept: text/*, application/json + * req.accepts('image/png'); + * req.accepts('png'); + * // => false + * + * // Accept: text/*;q=.5, application/json + * req.accepts(['html', 'json']); + * req.accepts('html, json'); + * // => "json" + */ + accepts(): string[]; + accepts(type: string): string | false; + accepts(type: string[]): string | false; + accepts(...type: string[]): string | false; + + /** + * Returns the first accepted charset of the specified character sets, + * based on the request's Accept-Charset HTTP header field. + * If none of the specified charsets is accepted, returns false. + * + * For more information, or if you have issues or concerns, see accepts. + */ + acceptsCharsets(): string[]; + acceptsCharsets(charset: string): string | false; + acceptsCharsets(charset: string[]): string | false; + acceptsCharsets(...charset: string[]): string | false; + + /** + * Returns the first accepted encoding of the specified encodings, + * based on the request's Accept-Encoding HTTP header field. + * If none of the specified encodings is accepted, returns false. + * + * For more information, or if you have issues or concerns, see accepts. + */ + acceptsEncodings(): string[]; + acceptsEncodings(encoding: string): string | false; + acceptsEncodings(encoding: string[]): string | false; + acceptsEncodings(...encoding: string[]): string | false; + + /** + * Returns the first accepted language of the specified languages, + * based on the request's Accept-Language HTTP header field. + * If none of the specified languages is accepted, returns false. + * + * For more information, or if you have issues or concerns, see accepts. + */ + acceptsLanguages(): string[]; + acceptsLanguages(lang: string): string | false; + acceptsLanguages(lang: string[]): string | false; + acceptsLanguages(...lang: string[]): string | false; + + /** + * Parse Range header field, capping to the given `size`. + * + * Unspecified ranges such as "0-" require knowledge of your resource length. In + * the case of a byte range this is of course the total number of bytes. + * If the Range header field is not given `undefined` is returned. + * If the Range header field is given, return value is a result of range-parser. + * See more ./types/range-parser/index.d.ts + * + * NOTE: remember that ranges are inclusive, so for example "Range: users=0-3" + * should respond with 4 users when available, not 3. + */ + range(size: number, options?: RangeParserOptions): RangeParserRanges | RangeParserResult | undefined; + + /** + * Return an array of Accepted media types + * ordered from highest quality to lowest. + */ + accepted: MediaType[]; + + /** + * Check if the incoming request contains the "Content-Type" + * header field, and it contains the give mime `type`. + * + * Examples: + * + * // With Content-Type: text/html; charset=utf-8 + * req.is('html'); + * req.is('text/html'); + * req.is('text/*'); + * // => true + * + * // When Content-Type is application/json + * req.is('json'); + * req.is('application/json'); + * req.is('application/*'); + * // => true + * + * req.is('html'); + * // => false + */ + is(type: string | string[]): string | false | null; + + /** + * Return the protocol string "http" or "https" + * when requested with TLS. When the "trust proxy" + * setting is enabled the "X-Forwarded-Proto" header + * field will be trusted. If you're running behind + * a reverse proxy that supplies https for you this + * may be enabled. + */ + readonly protocol: string; + + /** + * Short-hand for: + * + * req.protocol == 'https' + */ + readonly secure: boolean; + + /** + * Return the remote address, or when + * "trust proxy" is `true` return + * the upstream addr. + * + * Value may be undefined if the `req.socket` is destroyed + * (for example, if the client disconnected). + */ + readonly ip: string | undefined; + + /** + * When "trust proxy" is `true`, parse + * the "X-Forwarded-For" ip address list. + * + * For example if the value were "client, proxy1, proxy2" + * you would receive the array `["client", "proxy1", "proxy2"]` + * where "proxy2" is the furthest down-stream. + */ + readonly ips: string[]; + + /** + * Return subdomains as an array. + * + * Subdomains are the dot-separated parts of the host before the main domain of + * the app. By default, the domain of the app is assumed to be the last two + * parts of the host. This can be changed by setting "subdomain offset". + * + * For example, if the domain is "tobi.ferrets.example.com": + * If "subdomain offset" is not set, req.subdomains is `["ferrets", "tobi"]`. + * If "subdomain offset" is 3, req.subdomains is `["tobi"]`. + */ + readonly subdomains: string[]; + + /** + * Short-hand for `url.parse(req.url).pathname`. + */ + readonly path: string; + + /** + * Contains the hostname derived from the `Host` HTTP header. + */ + readonly hostname: string; + + /** + * Contains the host derived from the `Host` HTTP header. + */ + readonly host: string; + + /** + * Check if the request is fresh, aka + * Last-Modified and/or the ETag + * still match. + */ + readonly fresh: boolean; + + /** + * Check if the request is stale, aka + * "Last-Modified" and / or the "ETag" for the + * resource has changed. + */ + readonly stale: boolean; + + /** + * Check if the request was an _XMLHttpRequest_. + */ + readonly xhr: boolean; + + // body: { username: string; password: string; remember: boolean; title: string; }; + body: ReqBody; + + // cookies: { string; remember: boolean; }; + cookies: any; + + method: string; + + params: P; + + query: ReqQuery; + + route: any; + + signedCookies: any; + + originalUrl: string; + + url: string; + + baseUrl: string; + + app: Application; + + /** + * After middleware.init executed, Request will contain res and next properties + * See: express/lib/middleware/init.js + */ + res?: Response | undefined; + next?: NextFunction | undefined; +} + +export interface MediaType { + value: string; + quality: number; + type: string; + subtype: string; +} + +export type Send> = (body?: ResBody) => T; + +export interface SendFileOptions extends SendOptions { + /** Object containing HTTP headers to serve with the file. */ + headers?: Record; +} + +export interface DownloadOptions extends SendOptions { + /** Object containing HTTP headers to serve with the file. The header `Content-Disposition` will be overridden by the filename argument. */ + headers?: Record; +} + +export interface Response< + ResBody = any, + LocalsObj extends Record = Record, + StatusCode extends number = number, +> extends http.ServerResponse, Express.Response { + /** + * Set status `code`. + */ + status(code: StatusCode): this; + + /** + * Set the response HTTP status code to `statusCode` and send its string representation as the response body. + * @link http://expressjs.com/4x/api.html#res.sendStatus + * + * Examples: + * + * res.sendStatus(200); // equivalent to res.status(200).send('OK') + * res.sendStatus(403); // equivalent to res.status(403).send('Forbidden') + * res.sendStatus(404); // equivalent to res.status(404).send('Not Found') + * res.sendStatus(500); // equivalent to res.status(500).send('Internal Server Error') + */ + sendStatus(code: StatusCode): this; + + /** + * Set Link header field with the given `links`. + * + * Examples: + * + * res.links({ + * next: 'http://api.example.com/users?page=2', + * last: 'http://api.example.com/users?page=5' + * }); + */ + links(links: any): this; + + /** + * Send a response. + * + * Examples: + * + * res.send(new Buffer('wahoo')); + * res.send({ some: 'json' }); + * res.send('

some html

'); + * res.status(404).send('Sorry, cant find that'); + */ + send: Send; + + /** + * Send JSON response. + * + * Examples: + * + * res.json(null); + * res.json({ user: 'tj' }); + * res.status(500).json('oh noes!'); + * res.status(404).json('I dont have that'); + */ + json: Send; + + /** + * Send JSON response with JSONP callback support. + * + * Examples: + * + * res.jsonp(null); + * res.jsonp({ user: 'tj' }); + * res.status(500).jsonp('oh noes!'); + * res.status(404).jsonp('I dont have that'); + */ + jsonp: Send; + + /** + * Transfer the file at the given `path`. + * + * Automatically sets the _Content-Type_ response header field. + * The callback `fn(err)` is invoked when the transfer is complete + * or when an error occurs. Be sure to check `res.headersSent` + * if you wish to attempt responding, as the header and some data + * may have already been transferred. + * + * Options: + * + * - `maxAge` defaulting to 0 (can be string converted by `ms`) + * - `root` root directory for relative filenames + * - `headers` object of headers to serve with file + * - `dotfiles` serve dotfiles, defaulting to false; can be `"allow"` to send them + * + * Other options are passed along to `send`. + * + * Examples: + * + * The following example illustrates how `res.sendFile()` may + * be used as an alternative for the `static()` middleware for + * dynamic situations. The code backing `res.sendFile()` is actually + * the same code, so HTTP cache support etc is identical. + * + * app.get('/user/:uid/photos/:file', function(req, res){ + * var uid = req.params.uid + * , file = req.params.file; + * + * req.user.mayViewFilesFrom(uid, function(yes){ + * if (yes) { + * res.sendFile('/uploads/' + uid + '/' + file); + * } else { + * res.send(403, 'Sorry! you cant see that.'); + * } + * }); + * }); + * + * @api public + */ + sendFile(path: string, fn?: Errback): void; + sendFile(path: string, options: SendFileOptions, fn?: Errback): void; + + /** + * Transfer the file at the given `path` as an attachment. + * + * Optionally providing an alternate attachment `filename`, + * and optional callback `fn(err)`. The callback is invoked + * when the data transfer is complete, or when an error has + * ocurred. Be sure to check `res.headersSent` if you plan to respond. + * + * The optional options argument passes through to the underlying + * res.sendFile() call, and takes the exact same parameters. + * + * This method uses `res.sendFile()`. + */ + download(path: string, fn?: Errback): void; + download(path: string, filename: string, fn?: Errback): void; + download(path: string, filename: string, options: DownloadOptions, fn?: Errback): void; + + /** + * Set _Content-Type_ response header with `type` through `mime.lookup()` + * when it does not contain "/", or set the Content-Type to `type` otherwise. + * + * Examples: + * + * res.type('.html'); + * res.type('html'); + * res.type('json'); + * res.type('application/json'); + * res.type('png'); + */ + contentType(type: string): this; + + /** + * Set _Content-Type_ response header with `type` through `mime.lookup()` + * when it does not contain "/", or set the Content-Type to `type` otherwise. + * + * Examples: + * + * res.type('.html'); + * res.type('html'); + * res.type('json'); + * res.type('application/json'); + * res.type('png'); + */ + type(type: string): this; + + /** + * Respond to the Acceptable formats using an `obj` + * of mime-type callbacks. + * + * This method uses `req.accepted`, an array of + * acceptable types ordered by their quality values. + * When "Accept" is not present the _first_ callback + * is invoked, otherwise the first match is used. When + * no match is performed the server responds with + * 406 "Not Acceptable". + * + * Content-Type is set for you, however if you choose + * you may alter this within the callback using `res.type()` + * or `res.set('Content-Type', ...)`. + * + * res.format({ + * 'text/plain': function(){ + * res.send('hey'); + * }, + * + * 'text/html': function(){ + * res.send('

hey

'); + * }, + * + * 'appliation/json': function(){ + * res.send({ message: 'hey' }); + * } + * }); + * + * In addition to canonicalized MIME types you may + * also use extnames mapped to these types: + * + * res.format({ + * text: function(){ + * res.send('hey'); + * }, + * + * html: function(){ + * res.send('

hey

'); + * }, + * + * json: function(){ + * res.send({ message: 'hey' }); + * } + * }); + * + * By default Express passes an `Error` + * with a `.status` of 406 to `next(err)` + * if a match is not made. If you provide + * a `.default` callback it will be invoked + * instead. + */ + format(obj: any): this; + + /** + * Set _Content-Disposition_ header to _attachment_ with optional `filename`. + */ + attachment(filename?: string): this; + + /** + * Set header `field` to `val`, or pass + * an object of header fields. + * + * Examples: + * + * res.set('Foo', ['bar', 'baz']); + * res.set('Accept', 'application/json'); + * res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' }); + * + * Aliased as `res.header()`. + */ + set(field: any): this; + set(field: string, value?: string | string[]): this; + + header(field: any): this; + header(field: string, value?: string | string[]): this; + + // Property indicating if HTTP headers has been sent for the response. + headersSent: boolean; + + /** Get value for header `field`. */ + get(field: string): string | undefined; + + /** Clear cookie `name`. */ + clearCookie(name: string, options?: CookieOptions): this; + + /** + * Set cookie `name` to `val`, with the given `options`. + * + * Options: + * + * - `maxAge` max-age in milliseconds, converted to `expires` + * - `signed` sign the cookie + * - `path` defaults to "/" + * + * Examples: + * + * // "Remember Me" for 15 minutes + * res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true }); + * + * // save as above + * res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true }) + */ + cookie(name: string, val: string, options: CookieOptions): this; + cookie(name: string, val: any, options: CookieOptions): this; + cookie(name: string, val: any): this; + + /** + * Set the location header to `url`. + * + * Examples: + * + * res.location('/foo/bar').; + * res.location('http://example.com'); + * res.location('../login'); // /blog/post/1 -> /blog/login + * + * Mounting: + * + * When an application is mounted and `res.location()` + * is given a path that does _not_ lead with "/" it becomes + * relative to the mount-point. For example if the application + * is mounted at "/blog", the following would become "/blog/login". + * + * res.location('login'); + * + * While the leading slash would result in a location of "/login": + * + * res.location('/login'); + */ + location(url: string): this; + + /** + * Redirect to the given `url` with optional response `status` + * defaulting to 302. + * + * The resulting `url` is determined by `res.location()`, so + * it will play nicely with mounted apps, relative paths, etc. + * + * Examples: + * + * res.redirect('/foo/bar'); + * res.redirect('http://example.com'); + * res.redirect(301, 'http://example.com'); + * res.redirect('../login'); // /blog/post/1 -> /blog/login + */ + redirect(url: string): void; + redirect(status: number, url: string): void; + + /** + * Render `view` with the given `options` and optional callback `fn`. + * When a callback function is given a response will _not_ be made + * automatically, otherwise a response of _200_ and _text/html_ is given. + * + * Options: + * + * - `cache` boolean hinting to the engine it should cache + * - `filename` filename of the view being rendered + */ + render(view: string, options?: object, callback?: (err: Error, html: string) => void): void; + render(view: string, callback?: (err: Error, html: string) => void): void; + + locals: LocalsObj & Locals; + + charset: string; + + /** + * Adds the field to the Vary response header, if it is not there already. + * Examples: + * + * res.vary('User-Agent').render('docs'); + */ + vary(field: string): this; + + app: Application; + + /** + * Appends the specified value to the HTTP response header field. + * If the header is not already set, it creates the header with the specified value. + * The value parameter can be a string or an array. + * + * Note: calling res.set() after res.append() will reset the previously-set header value. + * + * @since 4.11.0 + */ + append(field: string, value?: string[] | string): this; + + /** + * After middleware.init executed, Response will contain req property + * See: express/lib/middleware/init.js + */ + req: Request; +} + +export interface Handler extends RequestHandler {} + +export type RequestParamHandler = (req: Request, res: Response, next: NextFunction, value: any, name: string) => any; + +export type ApplicationRequestHandler = + & IRouterHandler + & IRouterMatcher + & ((...handlers: RequestHandlerParams[]) => T); + +export interface Application< + LocalsObj extends Record = Record, +> extends EventEmitter, IRouter, Express.Application { + /** + * Express instance itself is a request handler, which could be invoked without + * third argument. + */ + (req: Request | http.IncomingMessage, res: Response | http.ServerResponse): any; + + /** + * Initialize the server. + * + * - setup default configuration + * - setup default middleware + * - setup route reflection methods + */ + init(): void; + + /** + * Initialize application configuration. + */ + defaultConfiguration(): void; + + /** + * Register the given template engine callback `fn` + * as `ext`. + * + * By default will `require()` the engine based on the + * file extension. For example if you try to render + * a "foo.jade" file Express will invoke the following internally: + * + * app.engine('jade', require('jade').__express); + * + * For engines that do not provide `.__express` out of the box, + * or if you wish to "map" a different extension to the template engine + * you may use this method. For example mapping the EJS template engine to + * ".html" files: + * + * app.engine('html', require('ejs').renderFile); + * + * In this case EJS provides a `.renderFile()` method with + * the same signature that Express expects: `(path, options, callback)`, + * though note that it aliases this method as `ejs.__express` internally + * so if you're using ".ejs" extensions you dont need to do anything. + * + * Some template engines do not follow this convention, the + * [Consolidate.js](https://github.com/visionmedia/consolidate.js) + * library was created to map all of node's popular template + * engines to follow this convention, thus allowing them to + * work seamlessly within Express. + */ + engine( + ext: string, + fn: (path: string, options: object, callback: (e: any, rendered?: string) => void) => void, + ): this; + + /** + * Assign `setting` to `val`, or return `setting`'s value. + * + * app.set('foo', 'bar'); + * app.get('foo'); + * // => "bar" + * app.set('foo', ['bar', 'baz']); + * app.get('foo'); + * // => ["bar", "baz"] + * + * Mounted servers inherit their parent server's settings. + */ + set(setting: string, val: any): this; + get: ((name: string) => any) & IRouterMatcher; + + param(name: string | string[], handler: RequestParamHandler): this; + + /** + * Return the app's absolute pathname + * based on the parent(s) that have + * mounted it. + * + * For example if the application was + * mounted as "/admin", which itself + * was mounted as "/blog" then the + * return value would be "/blog/admin". + */ + path(): string; + + /** + * Check if `setting` is enabled (truthy). + * + * app.enabled('foo') + * // => false + * + * app.enable('foo') + * app.enabled('foo') + * // => true + */ + enabled(setting: string): boolean; + + /** + * Check if `setting` is disabled. + * + * app.disabled('foo') + * // => true + * + * app.enable('foo') + * app.disabled('foo') + * // => false + */ + disabled(setting: string): boolean; + + /** Enable `setting`. */ + enable(setting: string): this; + + /** Disable `setting`. */ + disable(setting: string): this; + + /** + * Render the given view `name` name with `options` + * and a callback accepting an error and the + * rendered template string. + * + * Example: + * + * app.render('email', { name: 'Tobi' }, function(err, html){ + * // ... + * }) + */ + render(name: string, options?: object, callback?: (err: Error, html: string) => void): void; + render(name: string, callback: (err: Error, html: string) => void): void; + + /** + * Listen for connections. + * + * A node `http.Server` is returned, with this + * application (which is a `Function`) as its + * callback. If you wish to create both an HTTP + * and HTTPS server you may do so with the "http" + * and "https" modules as shown here: + * + * var http = require('http') + * , https = require('https') + * , express = require('express') + * , app = express(); + * + * http.createServer(app).listen(80); + * https.createServer({ ... }, app).listen(443); + */ + listen(port: number, hostname: string, backlog: number, callback?: (error?: Error) => void): http.Server; + listen(port: number, hostname: string, callback?: (error?: Error) => void): http.Server; + listen(port: number, callback?: (error?: Error) => void): http.Server; + listen(callback?: (error?: Error) => void): http.Server; + listen(path: string, callback?: (error?: Error) => void): http.Server; + listen(handle: any, listeningListener?: (error?: Error) => void): http.Server; + + router: Router; + + settings: any; + + resource: any; + + map: any; + + locals: LocalsObj & Locals; + + /** + * The app.routes object houses all of the routes defined mapped by the + * associated HTTP verb. This object may be used for introspection + * capabilities, for example Express uses this internally not only for + * routing but to provide default OPTIONS behaviour unless app.options() + * is used. Your application or framework may also remove routes by + * simply by removing them from this object. + */ + routes: any; + + /** + * Used to get all registered routes in Express Application + */ + _router: any; + + use: ApplicationRequestHandler; + + /** + * The mount event is fired on a sub-app, when it is mounted on a parent app. + * The parent app is passed to the callback function. + * + * NOTE: + * Sub-apps will: + * - Not inherit the value of settings that have a default value. You must set the value in the sub-app. + * - Inherit the value of settings with no default value. + */ + on: (event: string, callback: (parent: Application) => void) => this; + + /** + * The app.mountpath property contains one or more path patterns on which a sub-app was mounted. + */ + mountpath: string | string[]; +} + +export interface Express extends Application { + request: Request; + response: Response; +} diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 00000000000..10a68a262dc --- /dev/null +++ b/index.d.ts @@ -0,0 +1,131 @@ +/* =================== USAGE =================== + + import express = require("express"); + var app = express(); + + =============================================== */ + +/// +/// + +import * as bodyParser from "body-parser"; +import * as core from "./core"; +// import * as qs from "qs"; // @see https://github.com/expressjs/discussions/issues/192#issuecomment-2708756867 +import * as serveStatic from "serve-static"; + +/** + * Creates an Express application. The express() function is a top-level function exported by the express module. + */ +declare function e(): core.Express; + +declare namespace e { + /** + * This is a built-in middleware function in Express. It parses incoming requests with JSON payloads and is based on body-parser. + * @since 4.16.0 + */ + var json: typeof bodyParser.json; + + /** + * This is a built-in middleware function in Express. It parses incoming requests with Buffer payloads and is based on body-parser. + * @since 4.17.0 + */ + var raw: typeof bodyParser.raw; + + /** + * This is a built-in middleware function in Express. It parses incoming requests with text payloads and is based on body-parser. + * @since 4.17.0 + */ + var text: typeof bodyParser.text; + + /** + * These are the exposed prototypes. + */ + var application: Application; + var request: Request; + var response: Response; + + /** + * This is a built-in middleware function in Express. It serves static files and is based on serve-static. + */ + var static: serveStatic.RequestHandlerConstructor; + + /** + * This is a built-in middleware function in Express. It parses incoming requests with urlencoded payloads and is based on body-parser. + * @since 4.16.0 + */ + var urlencoded: typeof bodyParser.urlencoded; + + /** + * This is a built-in middleware function in Express. It parses incoming request query parameters. + * @todo this seems to be outdated, not implemented and should be removed + * @link https://github.com/DefinitelyTyped/DefinitelyTyped/pull/43217 + * @link https://github.com/expressjs/discussions/issues/192#issuecomment-2708756867 + */ + // export function query(options: qs.IParseOptions | typeof qs.parse): Handler; + + export function Router(options?: RouterOptions): core.Router; + + interface RouterOptions { + /** + * Enable case sensitivity. + */ + caseSensitive?: boolean | undefined; + + /** + * Preserve the req.params values from the parent router. + * If the parent and the child have conflicting param names, the child’s value take precedence. + * + * @default false + * @since 4.5.0 + */ + mergeParams?: boolean | undefined; + + /** + * Enable strict routing. + */ + strict?: boolean | undefined; + } + + interface Application extends core.Application {} + interface CookieOptions extends core.CookieOptions {} + interface Errback extends core.Errback {} + interface ErrorRequestHandler< + P = core.ParamsDictionary, + ResBody = any, + ReqBody = any, + ReqQuery = core.Query, + Locals extends Record = Record, + > extends core.ErrorRequestHandler {} + interface Express extends core.Express {} + interface Handler extends core.Handler {} + interface IRoute extends core.IRoute {} + interface IRouter extends core.IRouter {} + interface IRouterHandler extends core.IRouterHandler {} + interface IRouterMatcher extends core.IRouterMatcher {} + interface MediaType extends core.MediaType {} + interface NextFunction extends core.NextFunction {} + interface Locals extends core.Locals {} + interface Request< + P = core.ParamsDictionary, + ResBody = any, + ReqBody = any, + ReqQuery = core.Query, + Locals extends Record = Record, + > extends core.Request {} + interface RequestHandler< + P = core.ParamsDictionary, + ResBody = any, + ReqBody = any, + ReqQuery = core.Query, + Locals extends Record = Record, + > extends core.RequestHandler {} + interface RequestParamHandler extends core.RequestParamHandler {} + interface Response< + ResBody = any, + Locals extends Record = Record, + > extends core.Response {} + interface Router extends core.Router {} + interface Send extends core.Send {} +} + +export = e; diff --git a/package.json b/package.json index a0cc7b69d2b..286b455346b 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,9 @@ "vary": "~1.1.2" }, "devDependencies": { + "@arethetypeswrong/cli": "^0.17.4", + "@types/node": "^22.13.10", + "@types/serve-static": "^1.15.7", "after": "0.8.2", "connect-redis": "8.0.1", "cookie-parser": "1.4.7", @@ -86,10 +89,12 @@ "History.md", "Readme.md", "index.js", + "index.d.ts", + "core.d.ts", "lib/" ], "scripts": { - "lint": "eslint .", + "lint": "eslint . && attw --pack", "test": "mocha --require test/support/env --reporter spec --check-leaks test/ test/acceptance/", "test-ci": "nyc --exclude examples --exclude test --exclude benchmarks --reporter=lcovonly --reporter=text npm test", "test-cov": "nyc --exclude examples --exclude test --exclude benchmarks --reporter=html --reporter=text npm test", From fda6ec210a41223b158067760f75032fc738a93a Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Sun, 9 Mar 2025 11:20:18 +0100 Subject: [PATCH 02/16] rm type for express.query (removed in 5.0.0-alpha.1) --- index.d.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/index.d.ts b/index.d.ts index 10a68a262dc..085c56d68c9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -10,7 +10,6 @@ import * as bodyParser from "body-parser"; import * as core from "./core"; -// import * as qs from "qs"; // @see https://github.com/expressjs/discussions/issues/192#issuecomment-2708756867 import * as serveStatic from "serve-static"; /** @@ -55,14 +54,6 @@ declare namespace e { */ var urlencoded: typeof bodyParser.urlencoded; - /** - * This is a built-in middleware function in Express. It parses incoming request query parameters. - * @todo this seems to be outdated, not implemented and should be removed - * @link https://github.com/DefinitelyTyped/DefinitelyTyped/pull/43217 - * @link https://github.com/expressjs/discussions/issues/192#issuecomment-2708756867 - */ - // export function query(options: qs.IParseOptions | typeof qs.parse): Handler; - export function Router(options?: RouterOptions): core.Router; interface RouterOptions { From aa8bce7c6910cb0d3bafda7dbf924d96faf5ed5a Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Sun, 9 Mar 2025 12:00:01 +0100 Subject: [PATCH 03/16] Minor: removing tslint comment (no tslint). --- core.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/core.d.ts b/core.d.ts index 9e31f573a63..2443cf7d427 100644 --- a/core.d.ts +++ b/core.d.ts @@ -56,7 +56,6 @@ export interface RequestHandler< ReqQuery = ParsedQs, LocalsObj extends Record = Record, > { - // tslint:disable-next-line callable-types (This is extended from and can't extend from a type alias in ts<2.2) ( req: Request, res: Response, From 96bb4cd83dffa162a5592fc02402743f360993a4 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Sun, 9 Mar 2025 12:40:11 +0100 Subject: [PATCH 04/16] Linting the type declaration files using typescript-eslint. --- .eslintrc.yml | 7 +++++++ core.d.ts | 6 ------ package.json | 4 +++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 70bc9a6e7e1..ade7c0faa6b 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -2,9 +2,16 @@ root: true env: es2022: true node: true +plugins: + - "@typescript-eslint" rules: eol-last: error eqeqeq: [error, allow-null] indent: [error, 2, { MemberExpression: "off", SwitchCase: 1 }] no-trailing-spaces: error no-unused-vars: [error, { vars: all, args: none, ignoreRestSiblings: true }] +overrides: + - files: ["*.ts"] + parser: "@typescript-eslint/parser" + rules: + no-unused-vars: off diff --git a/core.d.ts b/core.d.ts index 2443cf7d427..e953abd38eb 100644 --- a/core.d.ts +++ b/core.d.ts @@ -111,7 +111,6 @@ type ParseRouteParameters = string extends Route ? ParamsD & (Rest extends `${GetRouteParameter}${infer Next}` ? RouteParameters : unknown) : {}; -/* eslint-disable @definitelytyped/no-unnecessary-generics */ export interface IRouterMatcher< T, Method extends "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head" = any, @@ -178,7 +177,6 @@ export interface IRouterHandler { LocalsObj extends Record = Record, >( // (This generic is meant to be passed explicitly.) - // eslint-disable-next-line @definitelytyped/no-unnecessary-generics ...handlers: Array> ): T; < @@ -189,7 +187,6 @@ export interface IRouterHandler { LocalsObj extends Record = Record, >( // (This generic is meant to be passed explicitly.) - // eslint-disable-next-line @definitelytyped/no-unnecessary-generics ...handlers: Array> ): T; < @@ -200,7 +197,6 @@ export interface IRouterHandler { LocalsObj extends Record = Record, >( // (This generic is meant to be passed explicitly.) - // eslint-disable-next-line @definitelytyped/no-unnecessary-generics ...handlers: Array> ): T; < @@ -211,11 +207,9 @@ export interface IRouterHandler { LocalsObj extends Record = Record, >( // (This generic is meant to be passed explicitly.) - // eslint-disable-next-line @definitelytyped/no-unnecessary-generics ...handlers: Array> ): T; } -/* eslint-enable @definitelytyped/no-unnecessary-generics */ export interface IRouter extends RequestHandler { /** diff --git a/package.json b/package.json index 286b455346b..6cef672dbf0 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "cookie-parser": "1.4.7", "cookie-session": "2.0.0", "ejs": "3.1.10", - "eslint": "8.47.0", + "eslint": "8.57.1", "express-session": "1.18.1", "hbs": "4.2.0", "marked": "15.0.3", @@ -79,6 +79,8 @@ "nyc": "15.1.0", "pbkdf2-password": "1.2.1", "supertest": "6.3.0", + "typescript": "^5.8.2", + "typescript-eslint": "^8.26.0", "vhost": "~3.0.2" }, "engines": { From c7610079ac83cd16b5f648ab8b589f0a7dda07f3 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Sun, 9 Mar 2025 20:01:54 +0100 Subject: [PATCH 05/16] Narrowing the extensions for ESLint config overrides. --- .eslintrc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index ade7c0faa6b..4d22678263e 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -11,7 +11,7 @@ rules: no-trailing-spaces: error no-unused-vars: [error, { vars: all, args: none, ignoreRestSiblings: true }] overrides: - - files: ["*.ts"] + - files: ["*.d.ts"] parser: "@typescript-eslint/parser" rules: no-unused-vars: off From 1e1c7e036ad86fb5628394166beacede9815d033 Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Thu, 20 Mar 2025 08:14:16 +0100 Subject: [PATCH 06/16] rm prettier directive --- core.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/core.d.ts b/core.d.ts index e953abd38eb..bb15bdc7ee8 100644 --- a/core.d.ts +++ b/core.d.ts @@ -95,7 +95,6 @@ type GetRouteParameter = RemoveTail< `.${string}` >; -// prettier-ignore export type RouteParameters = Route extends `${infer Required}{${infer Optional}}${infer Next}` ? ParseRouteParameters & Partial> & RouteParameters : ParseRouteParameters; From cb1cb374297d90d3017e370595da550be32a57a4 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Thu, 20 Mar 2025 08:39:19 +0100 Subject: [PATCH 07/16] rm tripple slash directives and @types/* dependencies. --- core.d.ts | 1 - index.d.ts | 3 --- package.json | 2 -- 3 files changed, 6 deletions(-) diff --git a/core.d.ts b/core.d.ts index bb15bdc7ee8..1cf280ee293 100644 --- a/core.d.ts +++ b/core.d.ts @@ -1,5 +1,4 @@ // This extracts the core definitions from express to prevent a circular dependency between express and serve-static -/// import { SendOptions } from "send"; diff --git a/index.d.ts b/index.d.ts index 085c56d68c9..a7903181c86 100644 --- a/index.d.ts +++ b/index.d.ts @@ -5,9 +5,6 @@ =============================================== */ -/// -/// - import * as bodyParser from "body-parser"; import * as core from "./core"; import * as serveStatic from "serve-static"; diff --git a/package.json b/package.json index 2d2fcc8ae87..c5c1903255e 100644 --- a/package.json +++ b/package.json @@ -62,8 +62,6 @@ }, "devDependencies": { "@arethetypeswrong/cli": "^0.17.4", - "@types/node": "^22.13.10", - "@types/serve-static": "^1.15.7", "after": "0.8.2", "connect-redis": "^8.0.1", "cookie-parser": "1.4.7", From 76c2a3698aae453c82cbc8e0ed4b907282ebcf20 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Thu, 20 Mar 2025 12:29:54 +0100 Subject: [PATCH 08/16] Replacing DT internal link with an URL. --- core.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core.d.ts b/core.d.ts index 1cf280ee293..034304f9ff4 100644 --- a/core.d.ts +++ b/core.d.ts @@ -500,7 +500,7 @@ export interface Request< * the case of a byte range this is of course the total number of bytes. * If the Range header field is not given `undefined` is returned. * If the Range header field is given, return value is a result of range-parser. - * See more ./types/range-parser/index.d.ts + * @see https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/range-parser/index.d.ts * * NOTE: remember that ranges are inclusive, so for example "Range: users=0-3" * should respond with 4 users when available, not 3. From e767f3abde18442e79284514697d8fcb6732c2ce Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Thu, 20 Mar 2025 12:52:28 +0100 Subject: [PATCH 09/16] feat: Testing the types (adjusted copy of the original assertions and typescript config). --- .eslintrc.yml | 2 +- .github/workflows/ci.yml | 3 + .github/workflows/legacy.yml | 3 + package.json | 12 +- test/types.ts | 232 +++++++++++++++++++++++++++++++++++ tsconfig.json | 13 ++ 6 files changed, 262 insertions(+), 3 deletions(-) create mode 100644 test/types.ts create mode 100644 tsconfig.json diff --git a/.eslintrc.yml b/.eslintrc.yml index 4d22678263e..ade7c0faa6b 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -11,7 +11,7 @@ rules: no-trailing-spaces: error no-unused-vars: [error, { vars: all, args: none, ignoreRestSiblings: true }] overrides: - - files: ["*.d.ts"] + - files: ["*.ts"] parser: "@typescript-eslint/parser" rules: no-unused-vars: off diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e68bdfd724b..aaf0f93a808 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,6 +85,9 @@ jobs: path: ./coverage/lcov.info retention-days: 1 + - name: Test types + run: npm run test-types + coverage: needs: test runs-on: ubuntu-latest diff --git a/.github/workflows/legacy.yml b/.github/workflows/legacy.yml index 6139f2fd9b7..806449560c6 100644 --- a/.github/workflows/legacy.yml +++ b/.github/workflows/legacy.yml @@ -69,6 +69,9 @@ jobs: path: ./coverage/lcov.info retention-days: 1 + - name: Test types + run: npm run test-types + coverage: needs: test runs-on: ubuntu-latest diff --git a/package.json b/package.json index c5c1903255e..373a8edb5e4 100644 --- a/package.json +++ b/package.json @@ -62,12 +62,19 @@ }, "devDependencies": { "@arethetypeswrong/cli": "^0.17.4", + "@types/body-parser": "^1.19.5", + "@types/ejs": "^3.1.5", + "@types/node": "^22.13.10", + "@types/qs": "^6.9.18", + "@types/range-parser": "^1.2.7", + "@types/serve-static": "^1.15.7", "after": "0.8.2", "connect-redis": "^8.0.1", "cookie-parser": "1.4.7", "cookie-session": "2.0.0", "ejs": "^3.1.10", "eslint": "8.57.1", + "expect-type": "^1.2.0", "express-session": "^1.18.1", "hbs": "4.2.0", "marked": "^15.0.3", @@ -94,10 +101,11 @@ "lib/" ], "scripts": { - "lint": "eslint . && attw --pack", + "lint": "eslint .", "test": "mocha --require test/support/env --reporter spec --check-leaks test/ test/acceptance/", "test-ci": "nyc --exclude examples --exclude test --exclude benchmarks --reporter=lcovonly --reporter=text npm test", "test-cov": "nyc --exclude examples --exclude test --exclude benchmarks --reporter=html --reporter=text npm test", - "test-tap": "mocha --require test/support/env --reporter tap --check-leaks test/ test/acceptance/" + "test-tap": "mocha --require test/support/env --reporter tap --check-leaks test/ test/acceptance/", + "test-types": "tsc && attw --pack" } } diff --git a/test/types.ts b/test/types.ts new file mode 100644 index 00000000000..75eaf7f288b --- /dev/null +++ b/test/types.ts @@ -0,0 +1,232 @@ +import * as express from '../index'; +import * as http from 'http'; +import * as ejs from 'ejs'; +import { Request, ParamsArray, ParamsDictionary } from '../core'; +import { expectTypeOf } from 'expect-type'; + +namespace express_tests { + const app = express(); + + app.engine('html', ejs.renderFile); + + express.static.mime.define({ + 'application/fx': ['fx'] + }); + app.use('/static', express.static(__dirname + '/public')); + + // simple logger + app.use((req, res, next) => { + console.log('%s %s', req.method, req.url); + next(); + }); + + app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => { + console.error(err); + next(err); + }); + + app.get('/', (req, res) => { + res.send('hello world'); + }); + + // Accept json app-wide or on one endpoint. + app.use(express.json({ limit: "200kb" })); + app.post('/echo', express.json(), (req, res) => { + res.json(req.body); + }); + + // Accept urlencoded app-wide or on one endpoint. + app.use(express.urlencoded({ + extended: false, + parameterLimit: 16 + })); + app.post('/search', express.urlencoded(), (req, res) => { + res.json(Object.keys(req.body)); + }); + + const router = express.Router({ caseSensitive: true, mergeParams: true, strict: true }); + + const pathStr = 'test'; + const pathRE: RegExp = /test/; + const path = true ? pathStr : pathRE; + + router.get(path); + router.put(path); + router.post(path); + router.delete(path); + router.get(pathStr); + router.put(pathStr); + router.post(pathStr); + router.delete(pathStr); + router.get(pathRE); + router.put(pathRE); + router.post(pathRE); + router.delete(pathRE); + + router.use((req, res, next) => { next(); }); + router.route('/users') + .get((req, res, next) => { + const types: string[] = req.accepts(); + let type: string | false = req.accepts('json'); + type = req.accepts(['json', 'text']); + type = req.accepts('json', 'text'); + + const charsets: string[] = req.acceptsCharsets(); + let charset: string | false = req.acceptsCharsets('utf-8'); + charset = req.acceptsCharsets(['utf-8', 'utf-16']); + charset = req.acceptsCharsets('utf-8', 'utf-16'); + + const encodings: string[] = req.acceptsEncodings(); + let encoding: string | false = req.acceptsEncodings('gzip'); + encoding = req.acceptsEncodings(['gzip', 'deflate']); + encoding = req.acceptsEncodings('gzip', 'deflate'); + + const languages: string[] = req.acceptsLanguages(); + let language: string | false = req.acceptsLanguages('en'); + language = req.acceptsLanguages(['en', 'ja']); + language = req.acceptsLanguages('en', 'ja'); + + // downcasting + req.get('set-cookie') as undefined; + req.get('set-cookie') as string[]; + const setCookieHeader1 = req.get('set-cookie'); + if (setCookieHeader1 !== undefined) { + const setCookieHeader2: string[] = setCookieHeader1; + } + req.get('header') as undefined; + req.get('header') as string; + const header1 = req.get('header'); + if (header1 !== undefined) { + const header2: string = header1; + } + + // upcasting + const setCookieHeader3: string[] | undefined = req.get('set-cookie'); + const header3: string | undefined = req.header('header'); + + req.headers.existingHeader as string; + req.headers.nonExistingHeader as any as undefined; + + // Since 4.14.0 req.range() has options + req.range(2, { combine: true }); + + res.send(req.query['token']); + }); + + router.get('/user/:id', (req, res, next) => { + if (Number(req.params.id) === 0) next('route'); + else next(); + }, (req, res, next) => { + res.render('regular'); + }); + + // Params defaults to dictionary + router.get('/:foo', req => { + expectTypeOf(req.params.foo).toEqualTypeOf(); + // @ts-expect-error -- not Array + req.params[0]; + }); + + // Params can used as an array + router.get('/*', req => { + expectTypeOf(req.params[0]).toEqualTypeOf(); + expectTypeOf(req.params.length).toEqualTypeOf(); + }); + + // Params can used as an array and can be specified via an explicit param type (core) + router.get('/*', (req: Request) => { + expectTypeOf(req.params[0]).toEqualTypeOf(); + expectTypeOf(req.params.length).toEqualTypeOf(); + }); + + // Params can used as an array and can be specified via an explicit param type (express) + router.get('/*', (req: express.Request) => { + expectTypeOf(req.params[0]).toEqualTypeOf(); + expectTypeOf(req.params.length).toEqualTypeOf(); + }); + + // Params can be a custom type that conforms to constraint + router.get<{ foo: string }>('/:foo', req => { + expectTypeOf(req.params.foo).toEqualTypeOf(); + // @ts-expect-error + req.params.bar; + }); + + // Params can be a custom type that conforms to constraint and can be specified via an explicit param type (core) + router.get('/:foo', (req: Request<{ foo: string }>) => { + expectTypeOf(req.params.foo).toEqualTypeOf(); + // @ts-expect-error + req.params.bar; + }); + + // Params can be a custom type that conforms to constraint and can be specified via an explicit param type (express) + router.get('/:foo', (req: express.Request<{ foo: string }>) => { + expectTypeOf(req.params.foo).toEqualTypeOf(); + // @ts-expect-error + req.params.bar; + }); + + // Params cannot be a custom type that does not conform to constraint + // router.get<{ foo: number }>('/:foo', () => {}); // original line that is expected to have error, but it does not + expectTypeOf<{ foo: number }>().not.toMatchTypeOf(); + + // Response will default to any type + router.get("/", (req: Request, res: express.Response) => { + res.json({}); + }); + + // Response will be of Type provided + router.get("/", (req: Request, res: express.Response) => { + res.json(); + // @ts-expect-error + res.json(1); + // @ts-expect-error + res.send(1); + }); + + app.use((req, res, next) => { + // hacky trick, router is just a handler + router(req, res, next); + }); + + // Test append function + app.use((req, res, next) => { + res.append('Link', ['', '']); + res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly'); + res.append('Warning', '199 Miscellaneous warning'); + }); + + app.use(router); + + // Test req.res, req.next, res.req should exists after middleware.init + app.use((req, res) => { + req.res; + req.next; + res.req; + }); + + // Test mounting sub-apps + app.use('/sub-app', express()); + + // Test on mount event + app.on('mount', (parent) => true); + + // Test mountpath + const mountPath: string|string[] = app.mountpath; + + app.listen(3000); + + const next: express.NextFunction = () => { }; +} + +/*************************** + * * + * Test with other modules * + * * + ***************************/ + +namespace node_tests { + // http.createServer can take express application + const app: express.Application = express(); + http.createServer(app).listen(5678); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000000..03791a37722 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "commonjs", + "lib": ["es6"], + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noEmit": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./*.d.ts", "./test/types.ts"] +} From 185f1908da2570e53671534c3b7e0ea2e3a48940 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Thu, 20 Mar 2025 12:58:13 +0100 Subject: [PATCH 10/16] Adjusting expect-type methods (using shorthands and avoiding deprecations). --- test/types.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/test/types.ts b/test/types.ts index 75eaf7f288b..04851c66401 100644 --- a/test/types.ts +++ b/test/types.ts @@ -122,53 +122,53 @@ namespace express_tests { // Params defaults to dictionary router.get('/:foo', req => { - expectTypeOf(req.params.foo).toEqualTypeOf(); + expectTypeOf(req.params.foo).toBeString(); // @ts-expect-error -- not Array req.params[0]; }); // Params can used as an array router.get('/*', req => { - expectTypeOf(req.params[0]).toEqualTypeOf(); - expectTypeOf(req.params.length).toEqualTypeOf(); + expectTypeOf(req.params[0]).toBeString(); + expectTypeOf(req.params.length).toBeNumber(); }); // Params can used as an array and can be specified via an explicit param type (core) router.get('/*', (req: Request) => { - expectTypeOf(req.params[0]).toEqualTypeOf(); - expectTypeOf(req.params.length).toEqualTypeOf(); + expectTypeOf(req.params[0]).toBeString() + expectTypeOf(req.params.length).toBeNumber(); }); // Params can used as an array and can be specified via an explicit param type (express) router.get('/*', (req: express.Request) => { - expectTypeOf(req.params[0]).toEqualTypeOf(); - expectTypeOf(req.params.length).toEqualTypeOf(); + expectTypeOf(req.params[0]).toBeString(); + expectTypeOf(req.params.length).toBeNumber(); }); // Params can be a custom type that conforms to constraint router.get<{ foo: string }>('/:foo', req => { - expectTypeOf(req.params.foo).toEqualTypeOf(); + expectTypeOf(req.params.foo).toBeString(); // @ts-expect-error req.params.bar; }); // Params can be a custom type that conforms to constraint and can be specified via an explicit param type (core) router.get('/:foo', (req: Request<{ foo: string }>) => { - expectTypeOf(req.params.foo).toEqualTypeOf(); + expectTypeOf(req.params.foo).toBeString(); // @ts-expect-error req.params.bar; }); // Params can be a custom type that conforms to constraint and can be specified via an explicit param type (express) router.get('/:foo', (req: express.Request<{ foo: string }>) => { - expectTypeOf(req.params.foo).toEqualTypeOf(); + expectTypeOf(req.params.foo).toBeString(); // @ts-expect-error req.params.bar; }); // Params cannot be a custom type that does not conform to constraint // router.get<{ foo: number }>('/:foo', () => {}); // original line that is expected to have error, but it does not - expectTypeOf<{ foo: number }>().not.toMatchTypeOf(); + expectTypeOf<{ foo: number }>().not.toExtend(); // Response will default to any type router.get("/", (req: Request, res: express.Response) => { From 7995e0e882292bfd571aeb87b3e8cdb3ce1e8481 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Thu, 20 Mar 2025 13:25:36 +0100 Subject: [PATCH 11/16] Replacing ts-expect-error directive with expect-type assetions. --- test/types.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/test/types.ts b/test/types.ts index 04851c66401..a35a8afa3f2 100644 --- a/test/types.ts +++ b/test/types.ts @@ -123,8 +123,7 @@ namespace express_tests { // Params defaults to dictionary router.get('/:foo', req => { expectTypeOf(req.params.foo).toBeString(); - // @ts-expect-error -- not Array - req.params[0]; + expectTypeOf(req.params).not.toBeArray(); }); // Params can used as an array @@ -148,22 +147,19 @@ namespace express_tests { // Params can be a custom type that conforms to constraint router.get<{ foo: string }>('/:foo', req => { expectTypeOf(req.params.foo).toBeString(); - // @ts-expect-error - req.params.bar; + expectTypeOf(req.params).not.toHaveProperty("bar"); }); // Params can be a custom type that conforms to constraint and can be specified via an explicit param type (core) router.get('/:foo', (req: Request<{ foo: string }>) => { expectTypeOf(req.params.foo).toBeString(); - // @ts-expect-error - req.params.bar; + expectTypeOf(req.params).not.toHaveProperty("bar"); }); // Params can be a custom type that conforms to constraint and can be specified via an explicit param type (express) router.get('/:foo', (req: express.Request<{ foo: string }>) => { expectTypeOf(req.params.foo).toBeString(); - // @ts-expect-error - req.params.bar; + expectTypeOf(req.params).not.toHaveProperty("bar"); }); // Params cannot be a custom type that does not conform to constraint @@ -178,10 +174,8 @@ namespace express_tests { // Response will be of Type provided router.get("/", (req: Request, res: express.Response) => { res.json(); - // @ts-expect-error - res.json(1); - // @ts-expect-error - res.send(1); + expectTypeOf().not.toExtend[0]>(); + expectTypeOf().not.toExtend[0]>(); }); app.use((req, res, next) => { From bd14851c8b5701f3b757034eee5fff09f8574e53 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Thu, 20 Mar 2025 17:48:36 +0100 Subject: [PATCH 12/16] Assertions without negating the parameter type of res.json and res.send. --- test/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/types.ts b/test/types.ts index a35a8afa3f2..4698c2af5b7 100644 --- a/test/types.ts +++ b/test/types.ts @@ -174,8 +174,8 @@ namespace express_tests { // Response will be of Type provided router.get("/", (req: Request, res: express.Response) => { res.json(); - expectTypeOf().not.toExtend[0]>(); - expectTypeOf().not.toExtend[0]>(); + expectTypeOf(res.json).parameter(0).exclude(undefined).toBeString(); + expectTypeOf(res.send).parameter(0).exclude(undefined).toBeString(); }); app.use((req, res, next) => { From 3bbb1caf9bfbf6a5c255c1a28da136748b8e6ddd Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Thu, 20 Mar 2025 21:02:10 +0100 Subject: [PATCH 13/16] Ref: rewriting test assignments into assertions. --- test/types.ts | 166 ++++++++++++++++++++++---------------------------- 1 file changed, 72 insertions(+), 94 deletions(-) diff --git a/test/types.ts b/test/types.ts index 4698c2af5b7..1c0e243bf2d 100644 --- a/test/types.ts +++ b/test/types.ts @@ -7,117 +7,95 @@ import { expectTypeOf } from 'expect-type'; namespace express_tests { const app = express(); - app.engine('html', ejs.renderFile); + expectTypeOf(app.engine).toBeCallableWith('html', ejs.renderFile); - express.static.mime.define({ + expectTypeOf(express.static.mime.define).toBeCallableWith({ 'application/fx': ['fx'] }); - app.use('/static', express.static(__dirname + '/public')); + expectTypeOf(app.use).toBeCallableWith('/static', express.static(__dirname + '/public')); // simple logger app.use((req, res, next) => { - console.log('%s %s', req.method, req.url); - next(); + expectTypeOf(console.log).toBeCallableWith('%s %s', req.method, req.url); + expectTypeOf(next).toBeCallableWith(); }); - app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => { - console.error(err); - next(err); + expectTypeOf(app.use).toBeCallableWith((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => { + expectTypeOf(console.error).toBeCallableWith(err); + expectTypeOf(next).toBeCallableWith(err); }); app.get('/', (req, res) => { - res.send('hello world'); + expectTypeOf(res.send).toBeCallableWith('hello world'); }); // Accept json app-wide or on one endpoint. - app.use(express.json({ limit: "200kb" })); + expectTypeOf(app.use).toBeCallableWith(express.json({ limit: "200kb" })); app.post('/echo', express.json(), (req, res) => { - res.json(req.body); + expectTypeOf(res.json).toBeCallableWith(req.body); }); // Accept urlencoded app-wide or on one endpoint. - app.use(express.urlencoded({ + expectTypeOf(app.use).toBeCallableWith(express.urlencoded({ extended: false, parameterLimit: 16 })); app.post('/search', express.urlencoded(), (req, res) => { - res.json(Object.keys(req.body)); + expectTypeOf(res.json).toBeCallableWith(Object.keys(req.body)); }); const router = express.Router({ caseSensitive: true, mergeParams: true, strict: true }); - const pathStr = 'test'; - const pathRE: RegExp = /test/; - const path = true ? pathStr : pathRE; - - router.get(path); - router.put(path); - router.post(path); - router.delete(path); - router.get(pathStr); - router.put(pathStr); - router.post(pathStr); - router.delete(pathStr); - router.get(pathRE); - router.put(pathRE); - router.post(pathRE); - router.delete(pathRE); - - router.use((req, res, next) => { next(); }); + expectTypeOf(router.get).toBeCallableWith('test'); + expectTypeOf(router.put).toBeCallableWith('test'); + expectTypeOf(router.post).toBeCallableWith('test'); + expectTypeOf(router.delete).toBeCallableWith('test'); + expectTypeOf(router.get).toBeCallableWith(/test/); + expectTypeOf(router.put).toBeCallableWith(/test/); + expectTypeOf(router.post).toBeCallableWith(/test/); + expectTypeOf(router.delete).toBeCallableWith(/test/); + + + router.use((req, res, next) => { + expectTypeOf(next).toBeCallableWith(); + }); + router.route('/users') .get((req, res, next) => { - const types: string[] = req.accepts(); - let type: string | false = req.accepts('json'); - type = req.accepts(['json', 'text']); - type = req.accepts('json', 'text'); - - const charsets: string[] = req.acceptsCharsets(); - let charset: string | false = req.acceptsCharsets('utf-8'); - charset = req.acceptsCharsets(['utf-8', 'utf-16']); - charset = req.acceptsCharsets('utf-8', 'utf-16'); - - const encodings: string[] = req.acceptsEncodings(); - let encoding: string | false = req.acceptsEncodings('gzip'); - encoding = req.acceptsEncodings(['gzip', 'deflate']); - encoding = req.acceptsEncodings('gzip', 'deflate'); - - const languages: string[] = req.acceptsLanguages(); - let language: string | false = req.acceptsLanguages('en'); - language = req.acceptsLanguages(['en', 'ja']); - language = req.acceptsLanguages('en', 'ja'); - - // downcasting - req.get('set-cookie') as undefined; - req.get('set-cookie') as string[]; - const setCookieHeader1 = req.get('set-cookie'); - if (setCookieHeader1 !== undefined) { - const setCookieHeader2: string[] = setCookieHeader1; - } - req.get('header') as undefined; - req.get('header') as string; - const header1 = req.get('header'); - if (header1 !== undefined) { - const header2: string = header1; - } - - // upcasting - const setCookieHeader3: string[] | undefined = req.get('set-cookie'); - const header3: string | undefined = req.header('header'); - - req.headers.existingHeader as string; - req.headers.nonExistingHeader as any as undefined; + expectTypeOf(req.accepts()).toEqualTypeOf(); + expectTypeOf(req.accepts(['json', 'text'])).toEqualTypeOf(); + expectTypeOf(req.accepts('json', 'text')).toEqualTypeOf(); + + expectTypeOf(req.acceptsCharsets()).toEqualTypeOf(); + expectTypeOf(req.acceptsCharsets(['utf-8', 'utf-16'])).toEqualTypeOf(); + expectTypeOf(req.acceptsCharsets('utf-8', 'utf-16')).toEqualTypeOf(); + + expectTypeOf(req.acceptsEncodings()).toEqualTypeOf(); + expectTypeOf(req.acceptsEncodings(['gzip', 'deflate'])).toEqualTypeOf(); + expectTypeOf(req.acceptsEncodings('gzip', 'deflate')).toEqualTypeOf(); + + expectTypeOf(req.acceptsLanguages()).toEqualTypeOf(); + expectTypeOf(req.acceptsLanguages(['en', 'ja'])).toEqualTypeOf(); + expectTypeOf(req.acceptsLanguages('en', 'ja')).toEqualTypeOf(); + + expectTypeOf(req.get('set-cookie')).toEqualTypeOf() + expectTypeOf(req.header('header')).toEqualTypeOf(); + + expectTypeOf(req.headers.existingHeader).toEqualTypeOf() // Since 4.14.0 req.range() has options - req.range(2, { combine: true }); + expectTypeOf(req.range).toBeCallableWith(2, { combine: true }); - res.send(req.query['token']); + expectTypeOf(res.send).toBeCallableWith(req.query['token']); }); router.get('/user/:id', (req, res, next) => { - if (Number(req.params.id) === 0) next('route'); - else next(); + expectTypeOf(next).toBeCallableWith('route'); + expectTypeOf(next).toBeCallableWith('router'); + expectTypeOf(next).toBeCallableWith(); + expectTypeOf(next).toBeCallableWith(new Error('test')); }, (req, res, next) => { - res.render('regular'); + expectTypeOf(res.render).toBeCallableWith('regular'); }); // Params defaults to dictionary @@ -168,49 +146,49 @@ namespace express_tests { // Response will default to any type router.get("/", (req: Request, res: express.Response) => { - res.json({}); + expectTypeOf(res.json).toBeCallableWith({}); }); // Response will be of Type provided router.get("/", (req: Request, res: express.Response) => { - res.json(); + expectTypeOf(res.json).toBeCallableWith(); expectTypeOf(res.json).parameter(0).exclude(undefined).toBeString(); expectTypeOf(res.send).parameter(0).exclude(undefined).toBeString(); }); + // router is a handler app.use((req, res, next) => { - // hacky trick, router is just a handler - router(req, res, next); + expectTypeOf(router).toBeCallableWith(req, res, next); }); // Test append function app.use((req, res, next) => { - res.append('Link', ['', '']); - res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly'); - res.append('Warning', '199 Miscellaneous warning'); + expectTypeOf(res.append).toBeCallableWith('Link', ['', '']); + expectTypeOf(res.append).toBeCallableWith('Set-Cookie', 'foo=bar; Path=/; HttpOnly'); + expectTypeOf(res.append).toBeCallableWith('Warning', '199 Miscellaneous warning'); }); - app.use(router); + expectTypeOf(app.use).toBeCallableWith(router); // Test req.res, req.next, res.req should exists after middleware.init app.use((req, res) => { - req.res; - req.next; - res.req; + expectTypeOf(req).toHaveProperty("res") + expectTypeOf(req).toHaveProperty("next"); + expectTypeOf(res).toHaveProperty("req"); }); // Test mounting sub-apps - app.use('/sub-app', express()); + expectTypeOf(app.use).toBeCallableWith('/sub-app', express()); // Test on mount event - app.on('mount', (parent) => true); + expectTypeOf(app.on).toBeCallableWith('mount', (parent) => true); // Test mountpath - const mountPath: string|string[] = app.mountpath; + expectTypeOf(app.mountpath).toEqualTypeOf(); - app.listen(3000); + expectTypeOf(app.listen).toBeCallableWith(3000); - const next: express.NextFunction = () => { }; + expectTypeOf().toExtend<() => void>(); } /*************************** @@ -221,6 +199,6 @@ namespace express_tests { namespace node_tests { // http.createServer can take express application - const app: express.Application = express(); - http.createServer(app).listen(5678); + expectTypeOf(express).returns.toExtend(); + expectTypeOf(http.createServer).toBeCallableWith(express()); } From 718f35be1120632a9aebb00184f240c26381d1a3 Mon Sep 17 00:00:00 2001 From: Anna Bocharova Date: Fri, 21 Mar 2025 06:55:15 +0100 Subject: [PATCH 14/16] Add missing semicolons --- test/types.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/types.ts b/test/types.ts index 1c0e243bf2d..22ca53385ec 100644 --- a/test/types.ts +++ b/test/types.ts @@ -78,10 +78,10 @@ namespace express_tests { expectTypeOf(req.acceptsLanguages(['en', 'ja'])).toEqualTypeOf(); expectTypeOf(req.acceptsLanguages('en', 'ja')).toEqualTypeOf(); - expectTypeOf(req.get('set-cookie')).toEqualTypeOf() + expectTypeOf(req.get('set-cookie')).toEqualTypeOf(); expectTypeOf(req.header('header')).toEqualTypeOf(); - expectTypeOf(req.headers.existingHeader).toEqualTypeOf() + expectTypeOf(req.headers.existingHeader).toEqualTypeOf(); // Since 4.14.0 req.range() has options expectTypeOf(req.range).toBeCallableWith(2, { combine: true }); @@ -112,7 +112,7 @@ namespace express_tests { // Params can used as an array and can be specified via an explicit param type (core) router.get('/*', (req: Request) => { - expectTypeOf(req.params[0]).toBeString() + expectTypeOf(req.params[0]).toBeString(); expectTypeOf(req.params.length).toBeNumber(); }); @@ -172,7 +172,7 @@ namespace express_tests { // Test req.res, req.next, res.req should exists after middleware.init app.use((req, res) => { - expectTypeOf(req).toHaveProperty("res") + expectTypeOf(req).toHaveProperty("res"); expectTypeOf(req).toHaveProperty("next"); expectTypeOf(res).toHaveProperty("req"); }); From 559deba485a4c2741a471e45cb79044ecfa72831 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 21 Mar 2025 10:34:09 +0100 Subject: [PATCH 15/16] Restoring tripple slash directives to keep the copy as is for now. --- core.d.ts | 1 + index.d.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/core.d.ts b/core.d.ts index 034304f9ff4..6c7535544b7 100644 --- a/core.d.ts +++ b/core.d.ts @@ -1,4 +1,5 @@ // This extracts the core definitions from express to prevent a circular dependency between express and serve-static +/// import { SendOptions } from "send"; diff --git a/index.d.ts b/index.d.ts index a7903181c86..085c56d68c9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -5,6 +5,9 @@ =============================================== */ +/// +/// + import * as bodyParser from "body-parser"; import * as core from "./core"; import * as serveStatic from "serve-static"; From e1c0aab6ae15c8ee69a1f0a880dc105a021eab9f Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Fri, 21 Mar 2025 10:43:56 +0100 Subject: [PATCH 16/16] Minor grammar corrections and cleanup in test. --- test/types.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/test/types.ts b/test/types.ts index 22ca53385ec..1b0d014265f 100644 --- a/test/types.ts +++ b/test/types.ts @@ -14,7 +14,7 @@ namespace express_tests { }); expectTypeOf(app.use).toBeCallableWith('/static', express.static(__dirname + '/public')); - // simple logger + // Simple logger app.use((req, res, next) => { expectTypeOf(console.log).toBeCallableWith('%s %s', req.method, req.url); expectTypeOf(next).toBeCallableWith(); @@ -104,19 +104,19 @@ namespace express_tests { expectTypeOf(req.params).not.toBeArray(); }); - // Params can used as an array + // Params can be used as an array router.get('/*', req => { expectTypeOf(req.params[0]).toBeString(); expectTypeOf(req.params.length).toBeNumber(); }); - // Params can used as an array and can be specified via an explicit param type (core) + // Params can be used as an array and can be specified via an explicit param type (core) router.get('/*', (req: Request) => { expectTypeOf(req.params[0]).toBeString(); expectTypeOf(req.params.length).toBeNumber(); }); - // Params can used as an array and can be specified via an explicit param type (express) + // Params can be used as an array and can be specified via an explicit param type (express) router.get('/*', (req: express.Request) => { expectTypeOf(req.params[0]).toBeString(); expectTypeOf(req.params.length).toBeNumber(); @@ -141,7 +141,6 @@ namespace express_tests { }); // Params cannot be a custom type that does not conform to constraint - // router.get<{ foo: number }>('/:foo', () => {}); // original line that is expected to have error, but it does not expectTypeOf<{ foo: number }>().not.toExtend(); // Response will default to any type @@ -156,7 +155,7 @@ namespace express_tests { expectTypeOf(res.send).parameter(0).exclude(undefined).toBeString(); }); - // router is a handler + // Router is a handler app.use((req, res, next) => { expectTypeOf(router).toBeCallableWith(req, res, next); }); @@ -170,7 +169,7 @@ namespace express_tests { expectTypeOf(app.use).toBeCallableWith(router); - // Test req.res, req.next, res.req should exists after middleware.init + // Test req.res, req.next, res.req should exist after middleware.init app.use((req, res) => { expectTypeOf(req).toHaveProperty("res"); expectTypeOf(req).toHaveProperty("next"); @@ -183,7 +182,7 @@ namespace express_tests { // Test on mount event expectTypeOf(app.on).toBeCallableWith('mount', (parent) => true); - // Test mountpath + // Test mount path expectTypeOf(app.mountpath).toEqualTypeOf(); expectTypeOf(app.listen).toBeCallableWith(3000);