diff --git a/docs/api/response.md b/docs/api/response.md index e30b1f1f7..8030da30d 100644 --- a/docs/api/response.md +++ b/docs/api/response.md @@ -14,7 +14,6 @@ Response header object. Alias as `response.header`. - ### response.socket Response socket. Points to net.Socket instance as `request.socket`. @@ -214,6 +213,7 @@ ctx.set('Cache-Control', 'no-cache'); ``` ### response.append(field, value) + Append additional header `field` with value `val`. ```js diff --git a/example/extend/middleware.ts b/example/extend/middleware.ts index b62cfd860..05b708b56 100644 --- a/example/extend/middleware.ts +++ b/example/extend/middleware.ts @@ -1,4 +1,4 @@ -import { MiddlewareFunc, Context, type ContextDelegation } from '../../src/index.js'; +import { MiddlewareFunc, Context } from '../../src/index.js'; class CustomContext extends Context { // Add your custom properties and methods here @@ -7,9 +7,7 @@ class CustomContext extends Context { } } -type ICustomContext = CustomContext & ContextDelegation; - -export const middleware: MiddlewareFunc = async (ctx, next) => { +export const middleware: MiddlewareFunc = async (ctx, next) => { console.log('middleware start, %s', ctx.hello, ctx.writable); await next(); console.log('middleware end'); diff --git a/package.json b/package.json index e781c75cc..df1eb75e1 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,11 @@ }, "description": "Koa web app framework for https://eggjs.org", "scripts": { - "test": "npm run lint -- --fix && egg-bin test", - "ci": "npm run lint && egg-bin cov && npm run prepublishOnly && attw --pack", + "pretest": "npm run lint -- --fix", + "test": "egg-bin test", + "preci": "npm run lint", + "ci": "egg-bin cov", + "postci": "npm run prepublishOnly && attw --pack", "lint": "eslint src test --cache", "authors": "git log --format='%aN <%aE>' | sort -u > AUTHORS", "prepublishOnly": "tshy && tshy-after" @@ -35,7 +38,6 @@ "content-disposition": "~0.5.4", "content-type": "^1.0.5", "cookies": "^0.9.1", - "delegates": "^1.0.0", "destroy": "^1.0.4", "encodeurl": "^1.0.2", "escape-html": "^1.0.3", @@ -53,12 +55,12 @@ }, "devDependencies": { "@arethetypeswrong/cli": "^0.17.1", + "@eggjs/bin": "^7.0.1", "@eggjs/tsconfig": "^1.3.3", "@types/accepts": "^1.3.7", "@types/content-disposition": "^0.5.8", "@types/content-type": "^1.1.8", "@types/cookies": "^0.9.0", - "@types/delegates": "^1.0.3", "@types/destroy": "^1.0.3", "@types/encodeurl": "^1.0.2", "@types/escape-html": "^1.0.4", @@ -67,17 +69,16 @@ "@types/http-errors": "^2.0.4", "@types/koa-compose": "^3.2.8", "@types/mocha": "^10.0.1", - "@types/node": "^20.2.5", + "@types/node": "22", "@types/on-finished": "^2.3.4", "@types/parseurl": "^1.3.3", "@types/statuses": "^2.0.5", "@types/supertest": "^6.0.2", "@types/type-is": "^1.6.6", "@types/vary": "^1.1.3", - "egg-bin": "^6.4.0", "eslint": "^8.41.0", "eslint-config-egg": "14", - "mm": "^3.3.0", + "mm": "^4.0.1", "supertest": "^3.1.0", "tsd": "^0.31.0", "tshy": "3", diff --git a/src/application.ts b/src/application.ts index 003ba49f9..e6c4500b4 100644 --- a/src/application.ts +++ b/src/application.ts @@ -11,7 +11,7 @@ import onFinished from 'on-finished'; import statuses from 'statuses'; import compose from 'koa-compose'; import { HttpError } from 'http-errors'; -import { Context, type ContextDelegation } from './context.js'; +import { Context } from './context.js'; import { Request } from './request.js'; import { Response } from './response.js'; import type { CustomError, AnyProto } from './types.js'; @@ -21,13 +21,13 @@ const debug = debuglog('@eggjs/koa/application'); export type ProtoImplClass = new(...args: any[]) => T; export type Next = () => Promise; type _MiddlewareFunc = (ctx: T, next: Next) => Promise | void; -export type MiddlewareFunc = _MiddlewareFunc & { _name?: string }; +export type MiddlewareFunc = _MiddlewareFunc & { _name?: string }; /** * Expose `Application` class. * Inherits from `Emitter.prototype`. */ -export class Application extends Emitter { +export class Application extends Emitter { [key: symbol]: unknown; /** * Make HttpError available to consumers of the library so that consumers don't @@ -41,14 +41,14 @@ export class Application extends Emitter { proxyIpHeader: string; maxIpsCount: number; protected _keys?: string[]; - middleware: MiddlewareFunc[]; - ctxStorage: AsyncLocalStorage; + middleware: MiddlewareFunc[]; + ctxStorage: AsyncLocalStorage; silent: boolean; - ContextClass: ProtoImplClass; + ContextClass: ProtoImplClass; context: AnyProto; - RequestClass: ProtoImplClass; + RequestClass: ProtoImplClass>; request: AnyProto; - ResponseClass: ProtoImplClass; + ResponseClass: ProtoImplClass>; response: AnyProto; /** @@ -84,11 +84,11 @@ export class Application extends Emitter { this.middleware = []; this.ctxStorage = getAsyncLocalStorage(); this.silent = false; - this.ContextClass = class ApplicationContext extends Context {} as any; + this.ContextClass = class ApplicationContext extends Context {} as ProtoImplClass; this.context = this.ContextClass.prototype; - this.RequestClass = class ApplicationRequest extends Request {}; + this.RequestClass = class ApplicationRequest extends Request {} as ProtoImplClass>; this.request = this.RequestClass.prototype; - this.ResponseClass = class ApplicationResponse extends Response {}; + this.ResponseClass = class ApplicationResponse extends Response {} as ProtoImplClass>; this.response = this.ResponseClass.prototype; } @@ -151,7 +151,7 @@ export class Application extends Emitter { /** * Use the given middleware `fn`. */ - use(fn: MiddlewareFunc) { + use(fn: MiddlewareFunc) { if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); const name = fn._name || fn.name || '-'; if (isGeneratorFunction(fn)) { @@ -196,7 +196,7 @@ export class Application extends Emitter { * Handle request in callback. * @private */ - protected async handleRequest(ctx: ContextDelegation, fnMiddleware: (ctx: ContextDelegation) => Promise) { + protected async handleRequest(ctx: T, fnMiddleware: (ctx: T) => Promise) { this.emit('request', ctx); const res = ctx.res; res.statusCode = 404; @@ -219,7 +219,7 @@ export class Application extends Emitter { * Initialize a new context. * @private */ - protected createContext(req: IncomingMessage, res: ServerResponse) { + createContext(req: IncomingMessage, res: ServerResponse) { const context = new this.ContextClass(this, req, res); return context; } @@ -246,7 +246,7 @@ export class Application extends Emitter { /** * Response helper. */ - protected _respond(ctx: ContextDelegation) { + protected _respond(ctx: T) { // allow bypassing koa if (ctx.respond === false) return; @@ -303,5 +303,3 @@ export class Application extends Emitter { res.end(body); } } - -export default Application; diff --git a/src/context.ts b/src/context.ts index 2d676f425..df013b00f 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,10 +1,11 @@ import util from 'node:util'; import type { IncomingMessage, ServerResponse } from 'node:http'; +import { ParsedUrlQuery } from 'node:querystring'; import createError from 'http-errors'; import httpAssert from 'http-assert'; -import delegate from 'delegates'; import statuses from 'statuses'; import Cookies from 'cookies'; +import { type Accepts } from 'accepts'; import type { Application } from './application.js'; import type { Request } from './request.js'; import type { Response } from './response.js'; @@ -222,69 +223,261 @@ export class Context { get state() { return this.#state; } -} -/** - * Request delegation. - */ - -delegate(Context.prototype, 'request') - .method('acceptsLanguages') - .method('acceptsEncodings') - .method('acceptsCharsets') - .method('accepts') - .method('get') - .method('is') - .access('querystring') - .access('idempotent') - .access('socket') - .access('search') - .access('method') - .access('query') - .access('path') - .access('url') - .access('accept') - .getter('origin') - .getter('href') - .getter('subdomains') - .getter('protocol') - .getter('host') - .getter('hostname') - .getter('URL') - .getter('header') - .getter('headers') - .getter('secure') - .getter('stale') - .getter('fresh') - .getter('ips') - .getter('ip'); - -/** - * Response delegation. - */ - -delegate(Context.prototype, 'response') - .method('attachment') - .method('redirect') - .method('remove') - .method('vary') - .method('has') - .method('set') - .method('append') - .method('flushHeaders') - .access('status') - .access('message') - .access('body') - .access('length') - .access('type') - .access('lastModified') - .access('etag') - .getter('headerSent') - .getter('writable'); - -export type ContextDelegation = Context & Pick -& Pick; + /** + * Request delegation. + */ + + acceptsLanguages(): string[]; + acceptsLanguages(languages: string[]): string | false; + acceptsLanguages(...languages: string[]): string | false; + acceptsLanguages(languages?: string | string[], ...others: string[]): string | string[] | false { + return this.request.acceptsLanguages(languages as any, ...others); + } + + acceptsEncodings(): string[]; + acceptsEncodings(encodings: string[]): string | false; + acceptsEncodings(...encodings: string[]): string | false; + acceptsEncodings(encodings?: string | string[], ...others: string[]): string[] | string | false { + return this.request.acceptsEncodings(encodings as any, ...others); + } + + acceptsCharsets(): string[]; + acceptsCharsets(charsets: string[]): string | false; + acceptsCharsets(...charsets: string[]): string | false; + acceptsCharsets(charsets?: string | string[], ...others: string[]): string[] | string | false { + return this.request.acceptsCharsets(charsets as any, ...others); + } + + accepts(...args: Parameters): string | string[] | false { + return this.request.accepts(...args); + } + + get(field: string): T { + return this.request.get(field); + } + + is(type?: string | string[], ...types: string[]): string | false | null { + return this.request.is(type, ...types); + } + + get querystring(): string { + return this.request.querystring; + } + + set querystring(str: string) { + this.request.querystring = str; + } + + get idempotent(): boolean { + return this.request.idempotent; + } + + get socket() { + return this.request.socket; + } + + get search(): string { + return this.request.search; + } + + set search(str: string) { + this.request.search = str; + } + + get method(): string { + return this.request.method; + } + + set method(method: string) { + this.request.method = method; + } + + get query(): ParsedUrlQuery { + return this.request.query; + } + + set query(obj: ParsedUrlQuery) { + this.request.query = obj; + } + + get path(): string { + return this.request.path; + } + + set path(path: string) { + this.request.path = path; + } + + get url(): string { + return this.request.url; + } + + set url(url: string) { + this.request.url = url; + } + + get accept(): Accepts { + return this.request.accept; + } + + set accept(accept: Accepts) { + this.request.accept = accept; + } + + get origin(): string { + return this.request.origin; + } + + get href(): string { + return this.request.href; + } + + get subdomains(): string[] { + return this.request.subdomains; + } + + get protocol(): string { + return this.request.protocol; + } + + get host(): string { + return this.request.host; + } + + get hostname(): string { + return this.request.hostname; + } + + get URL(): URL { + return this.request.URL; + } + + get header() { + return this.request.header; + } + + get headers() { + return this.request.headers; + } + + get secure(): boolean { + return this.request.secure; + } + + get stale(): boolean { + return this.request.stale; + } + + get fresh(): boolean { + return this.request.fresh; + } + + get ips(): string[] { + return this.request.ips; + } + + get ip(): string { + return this.request.ip; + } + + /** + * Response delegation. + */ + + attachment(...args: Parameters) { + return this.response.attachment(...args); + } + + redirect(...args: Parameters) { + return this.response.redirect(...args); + } + + remove(...args: Parameters) { + return this.response.remove(...args); + } + + vary(...args: Parameters) { + return this.response.vary(...args); + } + + has(...args: Parameters) { + return this.response.has(...args); + } + + set(...args: Parameters) { + return this.response.set(...args); + } + + append(...args: Parameters) { + return this.response.append(...args); + } + + flushHeaders(...args: Parameters) { + return this.response.flushHeaders(...args); + } + + get status() { + return this.response.status; + } + + set status(status: number) { + this.response.status = status; + } + + get message() { + return this.response.message; + } + + set message(msg: string) { + this.response.message = msg; + } + + get body(): any { + return this.response.body; + } + + set body(val: any) { + this.response.body = val; + } + + get length(): number | undefined { + return this.response.length; + } + + set length(n: number | string | undefined) { + this.response.length = n; + } + + get type(): string { + return this.response.type; + } + + set type(type: string | null | undefined) { + this.response.type = type; + } + + get lastModified() { + return this.response.lastModified; + } + + set lastModified(val: string | Date | undefined) { + this.response.lastModified = val; + } + + get etag() { + return this.response.etag; + } + + set etag(val: string) { + this.response.etag = val; + } + + get headerSent() { + return this.response.headerSent; + } + + get writable() { + return this.response.writable; + } +} diff --git a/src/request.ts b/src/request.ts index 0c16b49d0..e44a9f23f 100644 --- a/src/request.ts +++ b/src/request.ts @@ -9,23 +9,23 @@ import parse from 'parseurl'; import typeis from 'type-is'; import fresh from 'fresh'; import type { Application } from './application.js'; -import type { ContextDelegation } from './context.js'; +import type { Context } from './context.js'; import type { Response } from './response.js'; export interface RequestSocket extends Socket { encrypted: boolean; } -export class Request { +export class Request { [key: symbol]: unknown; app: Application; req: IncomingMessage; res: ServerResponse; - ctx: ContextDelegation; + ctx: T; response: Response; originalUrl: string; - constructor(app: Application, ctx: ContextDelegation, req: IncomingMessage, res: ServerResponse) { + constructor(app: Application, ctx: T, req: IncomingMessage, res: ServerResponse) { this.app = app; this.req = req; this.res = res; @@ -142,7 +142,7 @@ export class Request { /** * Get parsed query string. */ - get query() { + get query(): ParsedUrlQuery { const str = this.querystring; if (!this._parsedUrlQueryCache) { this._parsedUrlQueryCache = {}; @@ -158,7 +158,7 @@ export class Request { * Set query string as an object. */ - set query(obj) { + set query(obj: ParsedUrlQuery) { this.querystring = qs.stringify(obj); } diff --git a/src/response.ts b/src/response.ts index c3581756a..d070fe6fd 100644 --- a/src/response.ts +++ b/src/response.ts @@ -13,18 +13,18 @@ import destroy from 'destroy'; import vary from 'vary'; import encodeUrl from 'encodeurl'; import type { Application } from './application.js'; -import type { ContextDelegation } from './context.js'; +import type { Context } from './context.js'; import type { Request } from './request.js'; -export class Response { +export class Response { [key: symbol]: unknown; app: Application; req: IncomingMessage; res: ServerResponse; - ctx: ContextDelegation; + ctx: T; request: Request; - constructor(app: Application, ctx: ContextDelegation, req: IncomingMessage, res: ServerResponse) { + constructor(app: Application, ctx: T, req: IncomingMessage, res: ServerResponse) { this.app = app; this.req = req; this.res = res; @@ -168,16 +168,24 @@ export class Response { /** * Return parsed response Content-Length when present. + * + * When Content-Length is not defined it will return `undefined`. */ - get length() { + get length(): number | undefined { if (this.has('Content-Length')) { return parseInt(this.get('Content-Length'), 10) || 0; } const { body } = this; - if (!body || body instanceof Stream) return undefined; - if (typeof body === 'string') return Buffer.byteLength(body); - if (Buffer.isBuffer(body)) return body.length; + if (!body || body instanceof Stream) { + return undefined; + } + if (typeof body === 'string') { + return Buffer.byteLength(body); + } + if (Buffer.isBuffer(body)) { + return body.length; + } return Buffer.byteLength(JSON.stringify(body)); } @@ -270,7 +278,7 @@ export class Response { * Return the response mime type void of * parameters such as "charset". */ - get type() { + get type(): string { const type = this.get('Content-Type'); if (!type) return ''; return type.split(';', 1)[0]; diff --git a/test/application.test-d.ts b/test/application.test-d.ts index 5c798380b..0bf037437 100644 --- a/test/application.test-d.ts +++ b/test/application.test-d.ts @@ -1,11 +1,11 @@ import { expectType } from 'tsd'; -import { ContextDelegation } from '../src/index.js'; +import { Context } from '../src/index.js'; import Application from '../src/application.js'; -const ctx = {} as ContextDelegation; +const ctx = {} as Context; expectType(ctx.ip); expectType(ctx.app); const app = {} as Application; expectType(app.env); -expectType(app.ctxStorage.getStore()); +expectType(app.ctxStorage.getStore()); diff --git a/test/application/context.test.ts b/test/application/context.test.ts index c14572fbf..27087598b 100644 --- a/test/application/context.test.ts +++ b/test/application/context.test.ts @@ -1,6 +1,6 @@ import assert from 'node:assert'; import request from 'supertest'; -import { Application } from '../../src/index.js'; +import { Application, Context } from '../../src/index.js'; describe('app.context', () => { const app1 = new Application(); @@ -39,4 +39,30 @@ describe('app.context', () => { .get('/') .expect(204); }); + + describe('Sub Class', () => { + class MyContext extends Context { + getMsg() { + return 'world'; + } + } + + class MyApp extends Application { + constructor() { + super(); + this.ContextClass = MyContext; + } + } + + const app = new MyApp(); + app.use(ctx => { + ctx.body = `hello, ${ctx.getMsg()}`; + }); + + it('should work with sub class', () => { + return request(app.listen()) + .get('/') + .expect(200, 'hello, world'); + }); + }); }); diff --git a/test/test-helpers/context.ts b/test/test-helpers/context.ts index 570c75e55..758ba0dca 100644 --- a/test/test-helpers/context.ts +++ b/test/test-helpers/context.ts @@ -1,6 +1,5 @@ import stream from 'node:stream'; -import Koa from '../../src/application.js'; -import type { ContextDelegation } from '../../src/context.js'; +import { Application as Koa } from '../../src/application.js'; export default function context(req?: any, res?: any, app?: Koa) { const socket = new stream.Duplex(); @@ -23,7 +22,7 @@ export default function context(req?: any, res?: any, app?: Koa) { res.getHeaders = () => { return res._headers; }; - return (app as any).createContext(req, res) as ContextDelegation; + return app.createContext(req, res); } export function request(...args: any[]) {