From 4c4d584b9e93b350482dcb7ec2ad6fec9a5d5ff9 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Mon, 23 Dec 2024 17:00:49 +0800 Subject: [PATCH] pass context tests --- index-old.d.ts | 2536 ++++++++--------- package.json | 6 +- site/docs/advanced/framework.md | 62 +- site/docs/advanced/framework.zh-CN.md | 25 +- site/docs/basics/schedule.md | 2 +- site/docs/basics/schedule.zh-CN.md | 4 +- site/docs/core/unittest.md | 2 +- site/docs/core/unittest.zh-CN.md | 2 +- src/lib/core/messenger/IMessenger.ts | 4 +- src/lib/core/messenger/index.ts | 2 +- src/lib/core/messenger/ipc.ts | 39 +- src/lib/core/messenger/local.ts | 22 +- test/app/extend/context.test.ts | 4 +- test/app/extend/response.test.js | 87 - test/app/extend/response.test.ts | 115 + test/app/middleware/meta.test.ts | 2 +- test/bench/server.js | 2 +- .../apps/app-ts-type-check/xiandan.d.ts | 36 +- .../apps/app-ts-type-check/yadan.d.ts | 36 +- test/fixtures/apps/demo/app.js | 2 +- test/fixtures/apps/response/app/router.js | 6 +- test/lib/agent.test.js | 4 +- test/utils.ts | 102 +- 23 files changed, 1585 insertions(+), 1517 deletions(-) delete mode 100644 test/app/extend/response.test.js create mode 100644 test/app/extend/response.test.ts diff --git a/index-old.d.ts b/index-old.d.ts index b15296bd9a..b187499352 100644 --- a/index-old.d.ts +++ b/index-old.d.ts @@ -1,1268 +1,1268 @@ -import accepts = require('accepts'); -import { AsyncLocalStorage } from 'async_hooks'; -import { EventEmitter } from 'events'; -import { Readable } from 'stream'; -import { Socket } from 'net'; -import { IncomingMessage, ServerResponse } from 'http'; -import KoaApplication = require('koa'); -import KoaRouter = require('koa-router'); -import { - EggLogger as Logger, - EggLoggers, - LoggerLevel as EggLoggerLevel, - EggLoggersOptions, - EggLoggerOptions, - EggContextLogger, -} from 'egg-logger'; -import { - RequestOptions2 as RequestOptionsOld, - HttpClientResponse as HttpClientResponseOld, -} from 'urllib'; -import { - RequestURL as HttpClientRequestURL, - RequestOptions as HttpClientRequestOptions, - HttpClientResponse, -} from 'urllib-next'; -import { - EggCoreBase, - FileLoaderOption, - EggLoader as CoreLoader, - EggCoreOptions as CoreOptions, - EggLoaderOptions as CoreLoaderOptions, - BaseContextClass as CoreBaseContextClass, -} from 'egg-core'; -import EggCookies = require('egg-cookies'); -import 'egg-onerror'; -import 'egg-session'; -import 'egg-i18n'; -import '@eggjs/watcher'; -import 'egg-multipart'; -import 'egg-security'; -import 'egg-development'; -import 'egg-logrotator'; -import '@eggjs/schedule'; -import 'egg-static'; -import 'egg-jsonp'; -import 'egg-view'; - -declare module 'egg' { - export type EggLogger = Logger; - // plain object - type PlainObject = { [key: string]: T }; - - // Remove specific property from the specific class - type RemoveSpecProp = Pick>; - - // Usage: - // ```ts - // import { HttpClientRequestURL, HttpClientRequestOptions, HttpClientResponse } from 'egg'; - // async function request(url: HttpClientRequestURL, options: HttpClientRequestOptions): Promise { - // return await app.httpclient.request(url, options); - // } - // ``` - export { HttpClientRequestURL, HttpClientRequestOptions, HttpClientResponse }; - // Compatible with both urllib@2 and urllib@3 RequestOptions to request - export interface EggHttpClient extends EventEmitter { - request(url: HttpClientRequestURL): Promise | HttpClientResponse>; - request(url: HttpClientRequestURL, options: RequestOptionsOld | HttpClientRequestOptions): - Promise | HttpClientResponse>; - curl(url: HttpClientRequestURL): Promise | HttpClientResponse>; - curl(url: HttpClientRequestURL, options: RequestOptionsOld | HttpClientRequestOptions): - Promise | HttpClientResponse>; - } - - interface EggHttpConstructor { - new(app: Application): EggHttpClient; - } - - export interface EggContextHttpClient extends EggHttpClient { } - interface EggContextHttpClientConstructor { - new(ctx: Context): EggContextHttpClient; - } - - /** - * BaseContextClass is a base class that can be extended, - * it's instantiated in context level, - * {@link Helper}, {@link Service} is extending it. - */ - export class BaseContextClass extends CoreBaseContextClass { // tslint:disable-line - /** - * logger - */ - protected logger: EggLogger; - } - - export class Boot { - /** - * logger - * @member {EggLogger} - */ - protected logger: EggLogger; - - /** - * The configuration of application - * @member {EggAppConfig} - */ - protected config: EggAppConfig; - - /** - * The instance of agent - * @member {Agent} - */ - protected agent: Agent; - - /** - * The instance of app - * @member {Application} - */ - protected app: Application; - } - - export type RequestArrayBody = any[]; - export type RequestObjectBody = PlainObject; - export interface Request extends KoaApplication.Request { // tslint:disable-line - /** - * detect if response should be json - * 1. url path ends with `.json` - * 2. response type is set to json - * 3. detect by request accept header - * - * @member {Boolean} Request#acceptJSON - * @since 1.0.0 - */ - acceptJSON: boolean; - - /** - * Request remote IPv4 address - * @member {String} Request#ip - * @example - * ```js - * this.request.ip - * => '127.0.0.1' - * => '111.10.2.1' - * ``` - */ - ip: string; - - /** - * Get all pass through ip addresses from the request. - * Enable only on `app.config.proxy = true` - * - * @member {Array} Request#ips - * @example - * ```js - * this.request.ips - * => ['100.23.1.2', '201.10.10.2'] - * ``` - */ - ips: string[]; - - protocol: string; - - /** - * get params pass by querystring, all value are Array type. {@link Request#query} - * @member {Array} Request#queries - * @example - * ```js - * GET http://127.0.0.1:7001?a=b&a=c&o[foo]=bar&b[]=1&b[]=2&e=val - * this.queries - * => - * { - * "a": ["b", "c"], - * "o[foo]": ["bar"], - * "b[]": ["1", "2"], - * "e": ["val"] - * } - * ``` - */ - queries: PlainObject; - - /** - * get params pass by querystring, all value are String type. - * @member {Object} Request#query - * @example - * ```js - * GET http://127.0.0.1:7001?name=Foo&age=20&age=21 - * this.query - * => { 'name': 'Foo', 'age': 20 } - * - * GET http://127.0.0.1:7001?a=b&a=c&o[foo]=bar&b[]=1&b[]=2&e=val - * this.query - * => - * { - * "a": "b", - * "o[foo]": "bar", - * "b[]": "1", - * "e": "val" - * } - * ``` - */ - query: PlainObject; - - body: any; - } - - export interface Response extends KoaApplication.Response { // tslint:disable-line - /** - * read response real status code. - * - * e.g.: Using 302 status redirect to the global error page - * instead of show current 500 status page. - * And access log should save 500 not 302, - * then the `realStatus` can help us find out the real status code. - * @member {Number} Context#realStatus - */ - realStatus: number; - body: ResponseBodyT; - } - - export type LoggerLevel = EggLoggerLevel; - - - /** - * egg app info - * @example - * ```js - * // config/config.default.ts - * import { EggAppInfo } from 'egg'; - * - * export default (appInfo: EggAppInfo) => { - * return { - * keys: appInfo.name + '123456', - * }; - * } - * ``` - */ - export interface EggAppInfo { - pkg: any; // package.json - name: string; // the application name from package.json - baseDir: string; // current directory of application - env: EggEnvType; // equals to serverEnv - HOME: string; // home directory of the OS - root: string; // baseDir when local and unittest, HOME when other environment - } - - type IgnoreItem = string | RegExp | ((ctx: Context) => boolean); - type IgnoreOrMatch = IgnoreItem | IgnoreItem[]; - - /** Custom Loader Configuration */ - export interface CustomLoaderConfig extends RemoveSpecProp { - /** - * an object you wanner load to, value can only be 'ctx' or 'app'. default to app - */ - inject?: 'ctx' | 'app'; - /** - * whether need to load files in plugins or framework, default to false - */ - loadunit?: boolean; - } - - export interface HttpClientBaseConfig { - /** Whether use http keepalive */ - keepAlive?: boolean; - /** Free socket after keepalive timeout */ - freeSocketKeepAliveTimeout?: number; - /** Free socket after request timeout */ - freeSocketTimeout?: number; - /** Request timeout */ - timeout?: number; - /** Determines how many concurrent sockets the agent can have open per origin */ - maxSockets?: number; - /** The maximum number of sockets that will be left open in the free state */ - maxFreeSockets?: number; - } - - /** HttpClient config */ - export interface HttpClientConfig extends HttpClientBaseConfig { - /** http.Agent */ - httpAgent?: HttpClientBaseConfig; - /** https.Agent */ - httpsAgent?: HttpClientBaseConfig; - /** Default request args for httpclient */ - request?: HttpClientRequestOptions | RequestOptionsOld; - /** Whether enable dns cache */ - enableDNSCache?: boolean; - /** Enable proxy request, default is false. */ - enableProxy?: boolean; - /** proxy agent uri or options, default is null. */ - proxy?: string | { [key: string]: any }; - /** DNS cache lookup interval */ - dnsCacheLookupInterval?: number; - /** DNS cache max age */ - dnsCacheMaxLength?: number; - /** use urllib@3 HttpClient */ - useHttpClientNext?: boolean; - } - - export interface EggAppConfig { - workerStartTimeout: number; - baseDir: string; - middleware: string[]; - - /** - * The option of `bodyParser` middleware - * - * @member Config#bodyParser - * @property {Boolean} enable - enable bodyParser or not, default to true - * @property {String | RegExp | Function | Array} ignore - won't parse request body when url path hit ignore pattern, can not set `ignore` when `match` presented - * @property {String | RegExp | Function | Array} match - will parse request body only when url path hit match pattern - * @property {String} encoding - body encoding config, default utf8 - * @property {String} formLimit - form body size limit, default 1mb - * @property {String} jsonLimit - json body size limit, default 1mb - * @property {String} textLimit - json body size limit, default 1mb - * @property {Boolean} strict - json body strict mode, if set strict value true, then only receive object and array json body - * @property {Number} queryString.arrayLimit - from item array length limit, default 100 - * @property {Number} queryString.depth - json value deep length, default 5 - * @property {Number} queryString.parameterLimit - parameter number limit, default 1000 - * @property {String[]} enableTypes - parser will only parse when request type hits enableTypes, default is ['json', 'form'] - * @property {Object} extendTypes - support extend types - * @property {String} onProtoPoisoning - Defines what action must take when parsing a JSON object with `__proto__`. Possible values are `'error'`, `'remove'` and `'ignore'`. Default is `'error'`, it will return `400` response when `Prototype-Poisoning` happen. - */ - bodyParser: { - enable: boolean; - encoding: string; - formLimit: string; - jsonLimit: string; - textLimit: string; - strict: boolean; - queryString: { - arrayLimit: number; - depth: number; - parameterLimit: number; - }; - ignore: IgnoreOrMatch; - match: IgnoreOrMatch; - enableTypes: string[]; - extendTypes: { - json: string[]; - form: string[]; - text: string[]; - }; - /** Default is `'error'`, it will return `400` response when `Prototype-Poisoning` happen. */ - onProtoPoisoning: 'error' | 'remove' | 'ignore'; - }; - - /** - * logger options - * @member Config#logger - * @property {String} dir - directory of log files - * @property {String} encoding - log file encloding, defaults to utf8 - * @property {String} level - default log level, could be: DEBUG, INFO, WARN, ERROR or NONE, defaults to INFO in production - * @property {String} consoleLevel - log level of stdout, defaults to INFO in local serverEnv, defaults to WARN in unittest, defaults to NONE elsewise - * @property {Boolean} disableConsoleAfterReady - disable logger console after app ready. defaults to `false` on local and unittest env, others is `true`. - * @property {Boolean} outputJSON - log as JSON or not, defaults to false - * @property {Boolean} buffer - if enabled, flush logs to disk at a certain frequency to improve performance, defaults to true - * @property {String} errorLogName - file name of errorLogger - * @property {String} coreLogName - file name of coreLogger - * @property {String} agentLogName - file name of agent worker log - * @property {Object} coreLogger - custom config of coreLogger - * @property {Boolean} allowDebugAtProd - allow debug log at prod, defaults to false - * @property {Boolean} enableFastContextLogger - using the app logger instead of EggContextLogger, defaults to false - */ - logger: EggLoggerConfig; - - /** custom logger of egg */ - customLogger: { - [key: string]: EggLoggerOptions; - }; - - /** Configuration of httpclient in egg. */ - httpclient: HttpClientConfig; - - development: { - /** - * dirs needed watch, when files under these change, application will reload, use relative path - */ - watchDirs: string[]; - /** - * dirs don't need watch, including subdirectories, use relative path - */ - ignoreDirs: string[]; - /** - * don't wait all plugins ready, default is true. - */ - fastReady: boolean; - /** - * whether reload on debug, default is true. - */ - reloadOnDebug: boolean; - /** - * whether override default watchDirs, default is false. - */ - overrideDefault: boolean; - /** - * whether override default ignoreDirs, default is false. - */ - overrideIgnore: boolean; - /** - * whether to reload, use https://github.com/sindresorhus/multimatch - */ - reloadPattern: string[] | string; - }; - - /** - * customLoader config - */ - customLoader: { - [key: string]: CustomLoaderConfig; - }; - - /** - * It will ignore special keys when dumpConfig - */ - dump: { - ignore: Set; - }; - - /** - * The environment of egg - */ - env: EggEnvType; - - /** - * The current HOME directory - */ - HOME: string; - - hostHeaders: string; - - /** - * I18n options - */ - i18n: { - /** - * default value EN_US - */ - defaultLocale: string; - /** - * i18n resource file dir, not recommend to change default value - */ - dirs: string[]; - /** - * custom the locale value field, default `query.locale`, you can modify this config, such as `query.lang` - */ - queryField: string; - /** - * The locale value key in the cookie, default is locale. - */ - cookieField: string; - /** - * Locale cookie expire time, default `1y`, If pass number value, the unit will be ms - */ - cookieMaxAge: string | number; - }; - - /** - * Detect request' ip from specified headers, not case-sensitive. Only worked when config.proxy set to true. - */ - ipHeaders: string; - - /** - * jsonp options - * @member Config#jsonp - * @property {String} callback - jsonp callback method key, default to `_callback` - * @property {Number} limit - callback method name's max length, default to `50` - * @property {Boolean} csrf - enable csrf check or not. default to false - * @property {String|RegExp|Array} whiteList - referrer white list - */ - jsonp: { - limit: number; - callback: string; - csrf: boolean; - whiteList: string | RegExp | Array; - }; - - /** - * The key that signing cookies. It can contain multiple keys seperated by . - */ - keys: string; - - /** - * The name of the application - */ - name: string; - - /** - * package.json - */ - pkg: any; - - rundir: string; - - security: { - domainWhiteList: string[]; - protocolWhiteList: string[]; - defaultMiddleware: string; - csrf: any; - ssrf: { - ipBlackList: string[]; - ipExceptionList: string[]; - checkAddress?(ip: string): boolean; - }; - xframe: { - enable: boolean; - value: 'SAMEORIGIN' | 'DENY' | 'ALLOW-FROM'; - }; - hsts: any; - methodnoallow: { enable: boolean }; - noopen: { enable: boolean; } - xssProtection: any; - csp: any; - }; - - siteFile: PlainObject; - - watcher: PlainObject; - - onClientError(err: Error, socket: Socket, app: EggApplication): ClientErrorResponse | Promise; - - /** - * server timeout in milliseconds, default to 0 (no timeout). - * - * for special request, just use `ctx.req.setTimeout(ms)` - * - * @see https://nodejs.org/api/http.html#http_server_timeout - */ - serverTimeout: number | null; - - [prop: string]: any; - } - - export interface ClientErrorResponse { - body: string | Buffer; - status: number; - headers: { [key: string]: string }; - } - - export interface Router extends Omit, 'url'> { - /** - * restful router api - */ - resources(name: string, prefix: string, ...middleware: any[]): Router; - - /** - * @param {String} name - Router name - * @param {Object} [params] - more parameters - * @example - * ```js - * router.url('edit_post', { id: 1, name: 'foo', page: 2 }) - * => /posts/1/edit?name=foo&page=2 - * router.url('posts', { name: 'foo&1', page: 2 }) - * => /posts?name=foo%261&page=2 - * ``` - * @return {String} url by path name and query params. - * @since 1.0.0 - */ - url(name: string, params?: any): string; - /** - * Alias for the url method - */ - pathFor(name: string, params?: any): string; - methods: string[]; - } - - export interface EggApplication extends Omit, 'ctxStorage' | 'currentContext'> { - /** - * HttpClient instance - */ - httpclient: EggHttpClient; - - /** - * Logger for Application, wrapping app.coreLogger with context infomation - * - * @member {ContextLogger} Context#logger - * @since 1.0.0 - * @example - * ```js - * this.logger.info('some request data: %j', this.request.body); - * this.logger.warn('WARNING!!!!'); - * ``` - */ - logger: EggLogger; - - /** - * core logger for framework and plugins, log file is $HOME/logs/{appname}/egg-web - */ - coreLogger: EggLogger; - - /** - * All loggers contain logger, coreLogger and customLogger - */ - loggers: EggLoggers; - - /** - * messenger instance - */ - messenger: Messenger; - - /** - * get router - */ - router: Router; - - /** - * create a singleton instance - */ - addSingleton(name: string, create: any): void; - - runSchedule(schedulePath: string, ...args: any[]): Promise; - - /** - * http request helper base on httpclient, it will auto save httpclient log. - * Keep the same api with httpclient.request(url, args). - * See https://github.com/node-modules/urllib#api-doc for more details. - */ - curl: EggHttpClient['request']; - - /** - * Get logger by name, it's equal to app.loggers['name'], but you can extend it with your own logical - */ - getLogger(name: string): EggLogger; - - /** - * print the infomation when console.log(app) - */ - inspect(): any; - - /** - * Alias to Router#url - */ - url(name: string, params: any): any; - - /** - * Create an anonymous context, the context isn't request level, so the request is mocked. - * then you can use context level API like `ctx.service` - * @member {String} EggApplication#createAnonymousContext - * @param {Request} req - if you want to mock request like querystring, you can pass an object to this function. - * @return {Context} context - */ - createAnonymousContext(req?: Request): Context; - - /** - * export context base classes, let framework can impl sub class and over context extend easily. - */ - ContextCookies: typeof EggCookies; - ContextLogger: typeof EggContextLogger; - ContextHttpClient: EggContextHttpClientConstructor; - HttpClient: EggHttpConstructor; - Subscription: typeof Subscription; - Controller: typeof Controller; - Service: typeof Service; - } - - // compatible - export class EggApplication { - constructor(options?: CoreOptions); - } - - export type RouterPath = string | RegExp; - - export class Application extends EggApplication { - /** - * global locals for view - * @see Context#locals - */ - locals: IApplicationLocals; - - /** - * HTTP get method - */ - get(path: RouterPath, fn: string): void; - get(path: RouterPath, ...middleware: any[]): void; - - /** - * HTTP post method - */ - post(path: RouterPath, fn: string): void; - post(path: RouterPath, ...middleware: any[]): void; - - /** - * HTTP put method - */ - put(path: RouterPath, fn: string): void; - put(path: RouterPath, ...middleware: any[]): void; - - /** - * HTTP patch method - */ - patch(path: RouterPath, fn: string): void; - patch(path: RouterPath, ...middleware: any[]): void; - - /** - * HTTP delete method - */ - delete(path: RouterPath, fn: string): void; - delete(path: RouterPath, ...middleware: any[]): void; - - /** - * restful router api - */ - resources(name: string, prefix: string, fn: string): Router; - resources(path: string, prefix: string, ...middleware: any[]): Router; - - redirect(path: string, redirectPath: string): void; - - controller: IController; - - middleware: KoaApplication.Middleware[] & IMiddleware; - - /** - * Run async function in the background - * @see Context#runInBackground - * @param {Function} scope - the first args is an anonymous ctx - */ - runInBackground(scope: (ctx: Context) => void): void; - - /** - * Run async function in the anonymous context scope - * @see Context#runInAnonymousContextScope - * @param {Function} scope - the first args is an anonymous ctx, scope should be async function - * @param {Request} req - if you want to mock request like querystring, you can pass an object to this function. - */ - runInAnonymousContextScope(scope: (ctx: Context) => Promise, req?: Request): Promise; - - /** - * Get current execute ctx async local storage - * @return {AsyncLocalStorage} localStorage - store current execute Context - */ - get ctxStorage(): AsyncLocalStorage; - - /** - * Get current execute ctx, maybe undefined - * @return {Context} ctx - current execute Context - */ - get currentContext(): Context; - } - - export interface IApplicationLocals extends PlainObject { } - - export interface FileStream extends Readable { // tslint:disable-line - fields: any; - - filename: string; - - fieldname: string; - - mime: string; - - mimeType: string; - - transferEncoding: string; - - encoding: string; - - truncated: boolean; - } - - interface GetFileStreamOptions { - requireFile?: boolean; // required file submit, default is true - defCharset?: string; - limits?: { - fieldNameSize?: number; - fieldSize?: number; - fields?: number; - fileSize?: number; - files?: number; - parts?: number; - headerPairs?: number; - }; - checkFile?( - fieldname: string, - file: any, - filename: string, - encoding: string, - mimetype: string - ): void | Error; - } - - /** - * KoaApplication's Context will carry the default 'cookie' property in - * the egg's Context interface, which is wrong here because we have our own - * special properties (e.g: encrypted). So we must remove this property and - * create our own with the same name. - * @see https://github.com/eggjs/egg/pull/2958 - * - * However, the latest version of Koa has "[key: string]: any" on the - * context, and there'll be a type error for "keyof koa.Context". - * So we have to directly inherit from "KoaApplication.BaseContext" and - * rewrite all the properties to be compatible with types in Koa. - * @see https://github.com/eggjs/egg/pull/3329 - */ - export interface Context extends KoaApplication.BaseContext { - [key: string]: any; - body: ResponseBodyT; - - app: Application; - - // properties of koa.Context - req: IncomingMessage; - res: ServerResponse; - originalUrl: string; - respond?: boolean; - - service: IService; - - request: Request; - - response: Response; - - // The new 'cookies' instead of Koa's. - cookies: EggCookies; - - helper: IHelper; - - /** - * Resource Parameters - * @example - * ##### ctx.params.id {string} - * - * `GET /api/users/1` => `'1'` - * - * ##### ctx.params.ids {Array} - * - * `GET /api/users/1,2,3` => `['1', '2', '3']` - * - * ##### ctx.params.fields {Array} - * - * Expect request return data fields, for example - * `GET /api/users/1?fields=name,title` => `['name', 'title']`. - * - * ##### ctx.params.data {Object} - * - * Tht request data object - * - * ##### ctx.params.page {Number} - * - * Page number, `GET /api/users?page=10` => `10` - * - * ##### ctx.params.per_page {Number} - * - * The number of every page, `GET /api/users?per_page=20` => `20` - */ - params: any; - - /** - * @see Request#query - */ - query: PlainObject; - - /** - * @see Request#queries - */ - queries: PlainObject; - - /** - * @see Request#accept - */ - accept: accepts.Accepts; - - /** - * @see Request#acceptJSON - */ - acceptJSON: boolean; - - /** - * @see Request#ip - */ - ip: string; - - /** - * @see Response#realStatus - */ - realStatus: number; - - /** - * Set the ctx.body.data value - * - * @member {Object} Context#data= - * @example - * ```js - * ctx.data = { - * id: 1, - * name: 'fengmk2' - * }; - * ``` - * - * will get responce - * - * ```js - * HTTP/1.1 200 OK - * - * { - * "data": { - * "id": 1, - * "name": "fengmk2" - * } - * } - * ``` - */ - data: any; - - /** - * set ctx.body.meta value - * - * @example - * ```js - * ctx.meta = { - * count: 100 - * }; - * ``` - * will get responce - * - * ```js - * HTTP/1.1 200 OK - * - * { - * "meta": { - * "count": 100 - * } - * } - * ``` - */ - meta: any; - - /** - * locals is an object for view, you can use `app.locals` and `ctx.locals` to set variables, - * which will be used as data when view is rendering. - * The difference between `app.locals` and `ctx.locals` is the context level, `app.locals` is global level, and `ctx.locals` is request level. when you get `ctx.locals`, it will merge `app.locals`. - * - * when you set locals, only object is available - * - * ```js - * this.locals = { - * a: 1 - * }; - * this.locals = { - * b: 1 - * }; - * this.locals.c = 1; - * console.log(this.locals); - * { - * a: 1, - * b: 1, - * c: 1, - * }; - * ``` - * - * `ctx.locals` has cache, it only merges `app.locals` once in one request. - * - * @member {Object} Context#locals - */ - locals: IApplicationLocals & IContextLocals; - - /** - * alias to {@link locals}, compatible with koa that use this variable - */ - state: any; - - /** - * Logger for Application, wrapping app.coreLogger with context infomation - * - * @member {ContextLogger} Context#logger - * @since 1.0.0 - * @example - * ```js - * this.logger.info('some request data: %j', this.request.body); - * this.logger.warn('WARNING!!!!'); - * ``` - */ - logger: EggLogger; - - /** - * Get logger by name, it's equal to app.loggers['name'], but you can extend it with your own logical - */ - getLogger(name: string): EggLogger; - - /** - * Request start time - */ - starttime: number; - - /** - * Request start timer using `performance.now()` - */ - performanceStarttime?: number; - - /** - * http request helper base on httpclient, it will auto save httpclient log. - * Keep the same api with httpclient.request(url, args). - * See https://github.com/node-modules/urllib#api-doc for more details. - */ - curl: EggHttpClient['request']; - - __(key: string, ...values: string[]): string; - gettext(key: string, ...values: string[]): string; - - /** - * get upload file stream - * @example - * ```js - * const stream = await this.getFileStream(); - * // get other fields - * console.log(stream.fields); - * ``` - * @function Context#getFileStream - * @param {Object} options - * @return {ReadStream} stream - * @since 1.0.0 - */ - getFileStream(options?: GetFileStreamOptions): Promise; - - /** - * @see Responce.redirect - */ - redirect(url: string, alt?: string): void; - - httpclient: EggContextHttpClient; - } - - export interface IContextLocals extends PlainObject { } - - export class Controller extends BaseContextClass { } - - export class Service extends BaseContextClass { } - - export class Subscription extends BaseContextClass { } - - /** - * The empty interface `IService` is a placeholder, for egg - * to auto injection service to ctx.service - * - * @example - * - * import { Service } from 'egg'; - * class FooService extends Service { - * async bar() {} - * } - * - * declare module 'egg' { - * export interface IService { - * foo: FooService; - * } - * } - * - * Now I can get ctx.service.foo at controller and other service file. - */ - export interface IService extends PlainObject { } // tslint:disable-line - - export interface IController extends PlainObject { } // tslint:disable-line - - export interface IMiddleware extends PlainObject { } // tslint:disable-line - - export interface IHelper extends PlainObject, BaseContextClass { - /** - * Generate URL path(without host) for route. Takes the route name and a map of named params. - * @function Helper#pathFor - * @param {String} name - Router Name - * @param {Object} params - Other params - * - * @example - * ```js - * app.get('home', '/index.htm', 'home.index'); - * ctx.helper.pathFor('home', { by: 'recent', limit: 20 }) - * => /index.htm?by=recent&limit=20 - * ``` - * @return {String} url path(without host) - */ - pathFor(name: string, params?: PlainObject): string; - - /** - * Generate full URL(with host) for route. Takes the route name and a map of named params. - * @function Helper#urlFor - * @param {String} name - Router name - * @param {Object} params - Other params - * @example - * ```js - * app.get('home', '/index.htm', 'home.index'); - * ctx.helper.urlFor('home', { by: 'recent', limit: 20 }) - * => http://127.0.0.1:7001/index.htm?by=recent&limit=20 - * ``` - * @return {String} full url(with host) - */ - urlFor(name: string, params?: PlainObject): string; - } - - // egg env type - export type EggEnvType = 'local' | 'unittest' | 'prod' | string; - - /** - * plugin config item interface - */ - export interface IEggPluginItem { - env?: EggEnvType[]; - path?: string; - package?: string; - enable?: boolean; - } - - export type EggPluginItem = IEggPluginItem | boolean; - - /** - * build-in plugin list - */ - export interface EggPlugin { - [key: string]: EggPluginItem | undefined; - onerror?: EggPluginItem; - session?: EggPluginItem; - i18n?: EggPluginItem; - watcher?: EggPluginItem; - multipart?: EggPluginItem; - security?: EggPluginItem; - development?: EggPluginItem; - logrotator?: EggPluginItem; - schedule?: EggPluginItem; - static?: EggPluginItem; - jsonp?: EggPluginItem; - view?: EggPluginItem; - } - - /** - * Singleton instance in Agent Worker, extend {@link EggApplication} - */ - export class Agent extends EggApplication { - } - - export interface ClusterOptions { - /** specify framework that can be absolute path or npm package */ - framework?: string; - /** directory of application, default to `process.cwd()` */ - baseDir?: string; - /** customized plugins, for unittest */ - plugins?: object | null; - /** numbers of app workers, default to `os.cpus().length` */ - workers?: number; - /** listening port, default to 7001(http) or 8443(https) */ - port?: number; - /** https or not */ - https?: boolean; - /** ssl key */ - key?: string; - /** ssl cert */ - cert?: string; - [prop: string]: any; - } - - export function startCluster(options: ClusterOptions, callback: (...args: any[]) => any): void; - - export interface StartOptions { - /** specify framework that can be absolute path or npm package */ - framework?: string; - /** directory of application, default to `process.cwd()` */ - baseDir?: string; - /** ignore single process mode warning */ - ignoreWarning?: boolean; - } - - export function start(options?: StartOptions): Promise; - - /** - * Powerful Partial, Support adding ? modifier to a mapped property in deep level - * @example - * import { PowerPartial, EggAppConfig } from 'egg'; - * - * // { view: { defaultEngines: string } } => { view?: { defaultEngines?: string } } - * type EggConfig = PowerPartial - */ - export type PowerPartial = { - [U in keyof T]?: T[U] extends object - ? PowerPartial - : T[U] - }; - - // send data can be number|string|boolean|object but not Set|Map - export interface Messenger extends EventEmitter { - /** - * broadcast to all agent/app processes including itself - */ - broadcast(action: string, data: any): void; - - /** - * send to agent from the app, - * send to an random app from the agent - */ - sendRandom(action: string, data: any): void; - - /** - * send to specified process - */ - sendTo(pid: number, action: string, data: any): void; - - /** - * send to agent from the app, - * send to itself from the agent - */ - sendToAgent(action: string, data: any): void; - - /** - * send to all app including itself from the app, - * send to all app from the agent - */ - sendToApp(action: string, data: any): void; - } - - // compatible - export interface EggLoaderOptions extends CoreLoaderOptions { } - export interface EggLoader extends CoreLoader { } - - /** - * App worker process Loader, will load plugins - * @see https://github.com/eggjs/egg-core - */ - export class AppWorkerLoader extends CoreLoader { - loadConfig(): void; - load(): void; - } - - /** - * Agent worker process loader - * @see https://github.com/eggjs/egg-loader - */ - export class AgentWorkerLoader extends CoreLoader { - loadConfig(): void; - load(): void; - } - - export interface IBoot { - /** - * Ready to call configDidLoad, - * Config, plugin files are referred, - * this is the last chance to modify the config. - */ - configWillLoad?(): void; - - /** - * Config, plugin files have loaded - */ - configDidLoad?(): void; - - /** - * All files have loaded, start plugin here - */ - didLoad?(): Promise; - - /** - * All plugins have started, can do some thing before app ready - */ - willReady?(): Promise; - - /** - * Worker is ready, can do some things, - * don't need to block the app boot - */ - didReady?(): Promise; - - /** - * Server is listening - */ - serverDidReady?(): Promise; - - /** - * Do some thing before app close - */ - beforeClose?(): Promise; - } - - export interface Singleton { - get(id: string): T; - } -} +// import accepts = require('accepts'); +// import { AsyncLocalStorage } from 'async_hooks'; +// import { EventEmitter } from 'events'; +// import { Readable } from 'stream'; +// import { Socket } from 'net'; +// import { IncomingMessage, ServerResponse } from 'http'; +// import KoaApplication = require('koa'); +// import KoaRouter = require('koa-router'); +// import { +// EggLogger as Logger, +// EggLoggers, +// LoggerLevel as EggLoggerLevel, +// EggLoggersOptions, +// EggLoggerOptions, +// EggContextLogger, +// } from 'egg-logger'; +// import { +// RequestOptions2 as RequestOptionsOld, +// HttpClientResponse as HttpClientResponseOld, +// } from 'urllib'; +// import { +// RequestURL as HttpClientRequestURL, +// RequestOptions as HttpClientRequestOptions, +// HttpClientResponse, +// } from 'urllib-next'; +// import { +// EggCoreBase, +// FileLoaderOption, +// EggLoader as CoreLoader, +// EggCoreOptions as CoreOptions, +// EggLoaderOptions as CoreLoaderOptions, +// BaseContextClass as CoreBaseContextClass, +// } from 'egg-core'; +// import EggCookies = require('egg-cookies'); +// import 'egg-onerror'; +// import 'egg-session'; +// import 'egg-i18n'; +// import '@eggjs/watcher'; +// import 'egg-multipart'; +// import 'egg-security'; +// import 'egg-development'; +// import 'egg-logrotator'; +// import '@eggjs/schedule'; +// import 'egg-static'; +// import 'egg-jsonp'; +// import 'egg-view'; + +// declare module 'egg' { +// export type EggLogger = Logger; +// // plain object +// type PlainObject = { [key: string]: T }; + +// // Remove specific property from the specific class +// type RemoveSpecProp = Pick>; + +// // Usage: +// // ```ts +// // import { HttpClientRequestURL, HttpClientRequestOptions, HttpClientResponse } from 'egg'; +// // async function request(url: HttpClientRequestURL, options: HttpClientRequestOptions): Promise { +// // return await app.httpclient.request(url, options); +// // } +// // ``` +// export { HttpClientRequestURL, HttpClientRequestOptions, HttpClientResponse }; +// // Compatible with both urllib@2 and urllib@3 RequestOptions to request +// export interface EggHttpClient extends EventEmitter { +// request(url: HttpClientRequestURL): Promise | HttpClientResponse>; +// request(url: HttpClientRequestURL, options: RequestOptionsOld | HttpClientRequestOptions): +// Promise | HttpClientResponse>; +// curl(url: HttpClientRequestURL): Promise | HttpClientResponse>; +// curl(url: HttpClientRequestURL, options: RequestOptionsOld | HttpClientRequestOptions): +// Promise | HttpClientResponse>; +// } + +// interface EggHttpConstructor { +// new(app: Application): EggHttpClient; +// } + +// export interface EggContextHttpClient extends EggHttpClient { } +// interface EggContextHttpClientConstructor { +// new(ctx: Context): EggContextHttpClient; +// } + +// /** +// * BaseContextClass is a base class that can be extended, +// * it's instantiated in context level, +// * {@link Helper}, {@link Service} is extending it. +// */ +// export class BaseContextClass extends CoreBaseContextClass { // tslint:disable-line +// /** +// * logger +// */ +// protected logger: EggLogger; +// } + +// export class Boot { +// /** +// * logger +// * @member {EggLogger} +// */ +// protected logger: EggLogger; + +// /** +// * The configuration of application +// * @member {EggAppConfig} +// */ +// protected config: EggAppConfig; + +// /** +// * The instance of agent +// * @member {Agent} +// */ +// protected agent: Agent; + +// /** +// * The instance of app +// * @member {Application} +// */ +// protected app: Application; +// } + +// export type RequestArrayBody = any[]; +// export type RequestObjectBody = PlainObject; +// export interface Request extends KoaApplication.Request { // tslint:disable-line +// /** +// * detect if response should be json +// * 1. url path ends with `.json` +// * 2. response type is set to json +// * 3. detect by request accept header +// * +// * @member {Boolean} Request#acceptJSON +// * @since 1.0.0 +// */ +// acceptJSON: boolean; + +// /** +// * Request remote IPv4 address +// * @member {String} Request#ip +// * @example +// * ```js +// * this.request.ip +// * => '127.0.0.1' +// * => '111.10.2.1' +// * ``` +// */ +// ip: string; + +// /** +// * Get all pass through ip addresses from the request. +// * Enable only on `app.config.proxy = true` +// * +// * @member {Array} Request#ips +// * @example +// * ```js +// * this.request.ips +// * => ['100.23.1.2', '201.10.10.2'] +// * ``` +// */ +// ips: string[]; + +// protocol: string; + +// /** +// * get params pass by querystring, all value are Array type. {@link Request#query} +// * @member {Array} Request#queries +// * @example +// * ```js +// * GET http://127.0.0.1:7001?a=b&a=c&o[foo]=bar&b[]=1&b[]=2&e=val +// * this.queries +// * => +// * { +// * "a": ["b", "c"], +// * "o[foo]": ["bar"], +// * "b[]": ["1", "2"], +// * "e": ["val"] +// * } +// * ``` +// */ +// queries: PlainObject; + +// /** +// * get params pass by querystring, all value are String type. +// * @member {Object} Request#query +// * @example +// * ```js +// * GET http://127.0.0.1:7001?name=Foo&age=20&age=21 +// * this.query +// * => { 'name': 'Foo', 'age': 20 } +// * +// * GET http://127.0.0.1:7001?a=b&a=c&o[foo]=bar&b[]=1&b[]=2&e=val +// * this.query +// * => +// * { +// * "a": "b", +// * "o[foo]": "bar", +// * "b[]": "1", +// * "e": "val" +// * } +// * ``` +// */ +// query: PlainObject; + +// body: any; +// } + +// export interface Response extends KoaApplication.Response { // tslint:disable-line +// /** +// * read response real status code. +// * +// * e.g.: Using 302 status redirect to the global error page +// * instead of show current 500 status page. +// * And access log should save 500 not 302, +// * then the `realStatus` can help us find out the real status code. +// * @member {Number} Context#realStatus +// */ +// realStatus: number; +// body: ResponseBodyT; +// } + +// export type LoggerLevel = EggLoggerLevel; + + +// /** +// * egg app info +// * @example +// * ```js +// * // config/config.default.ts +// * import { EggAppInfo } from 'egg'; +// * +// * export default (appInfo: EggAppInfo) => { +// * return { +// * keys: appInfo.name + '123456', +// * }; +// * } +// * ``` +// */ +// export interface EggAppInfo { +// pkg: any; // package.json +// name: string; // the application name from package.json +// baseDir: string; // current directory of application +// env: EggEnvType; // equals to serverEnv +// HOME: string; // home directory of the OS +// root: string; // baseDir when local and unittest, HOME when other environment +// } + +// type IgnoreItem = string | RegExp | ((ctx: Context) => boolean); +// type IgnoreOrMatch = IgnoreItem | IgnoreItem[]; + +// /** Custom Loader Configuration */ +// export interface CustomLoaderConfig extends RemoveSpecProp { +// /** +// * an object you wanner load to, value can only be 'ctx' or 'app'. default to app +// */ +// inject?: 'ctx' | 'app'; +// /** +// * whether need to load files in plugins or framework, default to false +// */ +// loadunit?: boolean; +// } + +// export interface HttpClientBaseConfig { +// /** Whether use http keepalive */ +// keepAlive?: boolean; +// /** Free socket after keepalive timeout */ +// freeSocketKeepAliveTimeout?: number; +// /** Free socket after request timeout */ +// freeSocketTimeout?: number; +// /** Request timeout */ +// timeout?: number; +// /** Determines how many concurrent sockets the agent can have open per origin */ +// maxSockets?: number; +// /** The maximum number of sockets that will be left open in the free state */ +// maxFreeSockets?: number; +// } + +// /** HttpClient config */ +// export interface HttpClientConfig extends HttpClientBaseConfig { +// /** http.Agent */ +// httpAgent?: HttpClientBaseConfig; +// /** https.Agent */ +// httpsAgent?: HttpClientBaseConfig; +// /** Default request args for httpclient */ +// request?: HttpClientRequestOptions | RequestOptionsOld; +// /** Whether enable dns cache */ +// enableDNSCache?: boolean; +// /** Enable proxy request, default is false. */ +// enableProxy?: boolean; +// /** proxy agent uri or options, default is null. */ +// proxy?: string | { [key: string]: any }; +// /** DNS cache lookup interval */ +// dnsCacheLookupInterval?: number; +// /** DNS cache max age */ +// dnsCacheMaxLength?: number; +// /** use urllib@3 HttpClient */ +// useHttpClientNext?: boolean; +// } + +// export interface EggAppConfig { +// workerStartTimeout: number; +// baseDir: string; +// middleware: string[]; + +// /** +// * The option of `bodyParser` middleware +// * +// * @member Config#bodyParser +// * @property {Boolean} enable - enable bodyParser or not, default to true +// * @property {String | RegExp | Function | Array} ignore - won't parse request body when url path hit ignore pattern, can not set `ignore` when `match` presented +// * @property {String | RegExp | Function | Array} match - will parse request body only when url path hit match pattern +// * @property {String} encoding - body encoding config, default utf8 +// * @property {String} formLimit - form body size limit, default 1mb +// * @property {String} jsonLimit - json body size limit, default 1mb +// * @property {String} textLimit - json body size limit, default 1mb +// * @property {Boolean} strict - json body strict mode, if set strict value true, then only receive object and array json body +// * @property {Number} queryString.arrayLimit - from item array length limit, default 100 +// * @property {Number} queryString.depth - json value deep length, default 5 +// * @property {Number} queryString.parameterLimit - parameter number limit, default 1000 +// * @property {String[]} enableTypes - parser will only parse when request type hits enableTypes, default is ['json', 'form'] +// * @property {Object} extendTypes - support extend types +// * @property {String} onProtoPoisoning - Defines what action must take when parsing a JSON object with `__proto__`. Possible values are `'error'`, `'remove'` and `'ignore'`. Default is `'error'`, it will return `400` response when `Prototype-Poisoning` happen. +// */ +// bodyParser: { +// enable: boolean; +// encoding: string; +// formLimit: string; +// jsonLimit: string; +// textLimit: string; +// strict: boolean; +// queryString: { +// arrayLimit: number; +// depth: number; +// parameterLimit: number; +// }; +// ignore: IgnoreOrMatch; +// match: IgnoreOrMatch; +// enableTypes: string[]; +// extendTypes: { +// json: string[]; +// form: string[]; +// text: string[]; +// }; +// /** Default is `'error'`, it will return `400` response when `Prototype-Poisoning` happen. */ +// onProtoPoisoning: 'error' | 'remove' | 'ignore'; +// }; + +// /** +// * logger options +// * @member Config#logger +// * @property {String} dir - directory of log files +// * @property {String} encoding - log file encloding, defaults to utf8 +// * @property {String} level - default log level, could be: DEBUG, INFO, WARN, ERROR or NONE, defaults to INFO in production +// * @property {String} consoleLevel - log level of stdout, defaults to INFO in local serverEnv, defaults to WARN in unittest, defaults to NONE elsewise +// * @property {Boolean} disableConsoleAfterReady - disable logger console after app ready. defaults to `false` on local and unittest env, others is `true`. +// * @property {Boolean} outputJSON - log as JSON or not, defaults to false +// * @property {Boolean} buffer - if enabled, flush logs to disk at a certain frequency to improve performance, defaults to true +// * @property {String} errorLogName - file name of errorLogger +// * @property {String} coreLogName - file name of coreLogger +// * @property {String} agentLogName - file name of agent worker log +// * @property {Object} coreLogger - custom config of coreLogger +// * @property {Boolean} allowDebugAtProd - allow debug log at prod, defaults to false +// * @property {Boolean} enableFastContextLogger - using the app logger instead of EggContextLogger, defaults to false +// */ +// logger: EggLoggerConfig; + +// /** custom logger of egg */ +// customLogger: { +// [key: string]: EggLoggerOptions; +// }; + +// /** Configuration of httpclient in egg. */ +// httpclient: HttpClientConfig; + +// development: { +// /** +// * dirs needed watch, when files under these change, application will reload, use relative path +// */ +// watchDirs: string[]; +// /** +// * dirs don't need watch, including subdirectories, use relative path +// */ +// ignoreDirs: string[]; +// /** +// * don't wait all plugins ready, default is true. +// */ +// fastReady: boolean; +// /** +// * whether reload on debug, default is true. +// */ +// reloadOnDebug: boolean; +// /** +// * whether override default watchDirs, default is false. +// */ +// overrideDefault: boolean; +// /** +// * whether override default ignoreDirs, default is false. +// */ +// overrideIgnore: boolean; +// /** +// * whether to reload, use https://github.com/sindresorhus/multimatch +// */ +// reloadPattern: string[] | string; +// }; + +// /** +// * customLoader config +// */ +// customLoader: { +// [key: string]: CustomLoaderConfig; +// }; + +// /** +// * It will ignore special keys when dumpConfig +// */ +// dump: { +// ignore: Set; +// }; + +// /** +// * The environment of egg +// */ +// env: EggEnvType; + +// /** +// * The current HOME directory +// */ +// HOME: string; + +// hostHeaders: string; + +// /** +// * I18n options +// */ +// i18n: { +// /** +// * default value EN_US +// */ +// defaultLocale: string; +// /** +// * i18n resource file dir, not recommend to change default value +// */ +// dirs: string[]; +// /** +// * custom the locale value field, default `query.locale`, you can modify this config, such as `query.lang` +// */ +// queryField: string; +// /** +// * The locale value key in the cookie, default is locale. +// */ +// cookieField: string; +// /** +// * Locale cookie expire time, default `1y`, If pass number value, the unit will be ms +// */ +// cookieMaxAge: string | number; +// }; + +// /** +// * Detect request' ip from specified headers, not case-sensitive. Only worked when config.proxy set to true. +// */ +// ipHeaders: string; + +// /** +// * jsonp options +// * @member Config#jsonp +// * @property {String} callback - jsonp callback method key, default to `_callback` +// * @property {Number} limit - callback method name's max length, default to `50` +// * @property {Boolean} csrf - enable csrf check or not. default to false +// * @property {String|RegExp|Array} whiteList - referrer white list +// */ +// jsonp: { +// limit: number; +// callback: string; +// csrf: boolean; +// whiteList: string | RegExp | Array; +// }; + +// /** +// * The key that signing cookies. It can contain multiple keys seperated by . +// */ +// keys: string; + +// /** +// * The name of the application +// */ +// name: string; + +// /** +// * package.json +// */ +// pkg: any; + +// rundir: string; + +// security: { +// domainWhiteList: string[]; +// protocolWhiteList: string[]; +// defaultMiddleware: string; +// csrf: any; +// ssrf: { +// ipBlackList: string[]; +// ipExceptionList: string[]; +// checkAddress?(ip: string): boolean; +// }; +// xframe: { +// enable: boolean; +// value: 'SAMEORIGIN' | 'DENY' | 'ALLOW-FROM'; +// }; +// hsts: any; +// methodnoallow: { enable: boolean }; +// noopen: { enable: boolean; } +// xssProtection: any; +// csp: any; +// }; + +// siteFile: PlainObject; + +// watcher: PlainObject; + +// onClientError(err: Error, socket: Socket, app: EggApplication): ClientErrorResponse | Promise; + +// /** +// * server timeout in milliseconds, default to 0 (no timeout). +// * +// * for special request, just use `ctx.req.setTimeout(ms)` +// * +// * @see https://nodejs.org/api/http.html#http_server_timeout +// */ +// serverTimeout: number | null; + +// [prop: string]: any; +// } + +// export interface ClientErrorResponse { +// body: string | Buffer; +// status: number; +// headers: { [key: string]: string }; +// } + +// export interface Router extends Omit, 'url'> { +// /** +// * restful router api +// */ +// resources(name: string, prefix: string, ...middleware: any[]): Router; + +// /** +// * @param {String} name - Router name +// * @param {Object} [params] - more parameters +// * @example +// * ```js +// * router.url('edit_post', { id: 1, name: 'foo', page: 2 }) +// * => /posts/1/edit?name=foo&page=2 +// * router.url('posts', { name: 'foo&1', page: 2 }) +// * => /posts?name=foo%261&page=2 +// * ``` +// * @return {String} url by path name and query params. +// * @since 1.0.0 +// */ +// url(name: string, params?: any): string; +// /** +// * Alias for the url method +// */ +// pathFor(name: string, params?: any): string; +// methods: string[]; +// } + +// export interface EggApplication extends Omit, 'ctxStorage' | 'currentContext'> { +// /** +// * HttpClient instance +// */ +// httpclient: EggHttpClient; + +// /** +// * Logger for Application, wrapping app.coreLogger with context infomation +// * +// * @member {ContextLogger} Context#logger +// * @since 1.0.0 +// * @example +// * ```js +// * this.logger.info('some request data: %j', this.request.body); +// * this.logger.warn('WARNING!!!!'); +// * ``` +// */ +// logger: EggLogger; + +// /** +// * core logger for framework and plugins, log file is $HOME/logs/{appname}/egg-web +// */ +// coreLogger: EggLogger; + +// /** +// * All loggers contain logger, coreLogger and customLogger +// */ +// loggers: EggLoggers; + +// /** +// * messenger instance +// */ +// messenger: Messenger; + +// /** +// * get router +// */ +// router: Router; + +// /** +// * create a singleton instance +// */ +// addSingleton(name: string, create: any): void; + +// runSchedule(schedulePath: string, ...args: any[]): Promise; + +// /** +// * http request helper base on httpclient, it will auto save httpclient log. +// * Keep the same api with httpclient.request(url, args). +// * See https://github.com/node-modules/urllib#api-doc for more details. +// */ +// curl: EggHttpClient['request']; + +// /** +// * Get logger by name, it's equal to app.loggers['name'], but you can extend it with your own logical +// */ +// getLogger(name: string): EggLogger; + +// /** +// * print the infomation when console.log(app) +// */ +// inspect(): any; + +// /** +// * Alias to Router#url +// */ +// url(name: string, params: any): any; + +// /** +// * Create an anonymous context, the context isn't request level, so the request is mocked. +// * then you can use context level API like `ctx.service` +// * @member {String} EggApplication#createAnonymousContext +// * @param {Request} req - if you want to mock request like querystring, you can pass an object to this function. +// * @return {Context} context +// */ +// createAnonymousContext(req?: Request): Context; + +// /** +// * export context base classes, let framework can impl sub class and over context extend easily. +// */ +// ContextCookies: typeof EggCookies; +// ContextLogger: typeof EggContextLogger; +// ContextHttpClient: EggContextHttpClientConstructor; +// HttpClient: EggHttpConstructor; +// Subscription: typeof Subscription; +// Controller: typeof Controller; +// Service: typeof Service; +// } + +// // compatible +// export class EggApplication { +// constructor(options?: CoreOptions); +// } + +// export type RouterPath = string | RegExp; + +// export class Application extends EggApplication { +// /** +// * global locals for view +// * @see Context#locals +// */ +// locals: IApplicationLocals; + +// /** +// * HTTP get method +// */ +// get(path: RouterPath, fn: string): void; +// get(path: RouterPath, ...middleware: any[]): void; + +// /** +// * HTTP post method +// */ +// post(path: RouterPath, fn: string): void; +// post(path: RouterPath, ...middleware: any[]): void; + +// /** +// * HTTP put method +// */ +// put(path: RouterPath, fn: string): void; +// put(path: RouterPath, ...middleware: any[]): void; + +// /** +// * HTTP patch method +// */ +// patch(path: RouterPath, fn: string): void; +// patch(path: RouterPath, ...middleware: any[]): void; + +// /** +// * HTTP delete method +// */ +// delete(path: RouterPath, fn: string): void; +// delete(path: RouterPath, ...middleware: any[]): void; + +// /** +// * restful router api +// */ +// resources(name: string, prefix: string, fn: string): Router; +// resources(path: string, prefix: string, ...middleware: any[]): Router; + +// redirect(path: string, redirectPath: string): void; + +// controller: IController; + +// middleware: KoaApplication.Middleware[] & IMiddleware; + +// /** +// * Run async function in the background +// * @see Context#runInBackground +// * @param {Function} scope - the first args is an anonymous ctx +// */ +// runInBackground(scope: (ctx: Context) => void): void; + +// /** +// * Run async function in the anonymous context scope +// * @see Context#runInAnonymousContextScope +// * @param {Function} scope - the first args is an anonymous ctx, scope should be async function +// * @param {Request} req - if you want to mock request like querystring, you can pass an object to this function. +// */ +// runInAnonymousContextScope(scope: (ctx: Context) => Promise, req?: Request): Promise; + +// /** +// * Get current execute ctx async local storage +// * @return {AsyncLocalStorage} localStorage - store current execute Context +// */ +// get ctxStorage(): AsyncLocalStorage; + +// /** +// * Get current execute ctx, maybe undefined +// * @return {Context} ctx - current execute Context +// */ +// get currentContext(): Context; +// } + +// export interface IApplicationLocals extends PlainObject { } + +// export interface FileStream extends Readable { // tslint:disable-line +// fields: any; + +// filename: string; + +// fieldname: string; + +// mime: string; + +// mimeType: string; + +// transferEncoding: string; + +// encoding: string; + +// truncated: boolean; +// } + +// interface GetFileStreamOptions { +// requireFile?: boolean; // required file submit, default is true +// defCharset?: string; +// limits?: { +// fieldNameSize?: number; +// fieldSize?: number; +// fields?: number; +// fileSize?: number; +// files?: number; +// parts?: number; +// headerPairs?: number; +// }; +// checkFile?( +// fieldname: string, +// file: any, +// filename: string, +// encoding: string, +// mimetype: string +// ): void | Error; +// } + +// /** +// * KoaApplication's Context will carry the default 'cookie' property in +// * the egg's Context interface, which is wrong here because we have our own +// * special properties (e.g: encrypted). So we must remove this property and +// * create our own with the same name. +// * @see https://github.com/eggjs/egg/pull/2958 +// * +// * However, the latest version of Koa has "[key: string]: any" on the +// * context, and there'll be a type error for "keyof koa.Context". +// * So we have to directly inherit from "KoaApplication.BaseContext" and +// * rewrite all the properties to be compatible with types in Koa. +// * @see https://github.com/eggjs/egg/pull/3329 +// */ +// export interface Context extends KoaApplication.BaseContext { +// [key: string]: any; +// body: ResponseBodyT; + +// app: Application; + +// // properties of koa.Context +// req: IncomingMessage; +// res: ServerResponse; +// originalUrl: string; +// respond?: boolean; + +// service: IService; + +// request: Request; + +// response: Response; + +// // The new 'cookies' instead of Koa's. +// cookies: EggCookies; + +// helper: IHelper; + +// /** +// * Resource Parameters +// * @example +// * ##### ctx.params.id {string} +// * +// * `GET /api/users/1` => `'1'` +// * +// * ##### ctx.params.ids {Array} +// * +// * `GET /api/users/1,2,3` => `['1', '2', '3']` +// * +// * ##### ctx.params.fields {Array} +// * +// * Expect request return data fields, for example +// * `GET /api/users/1?fields=name,title` => `['name', 'title']`. +// * +// * ##### ctx.params.data {Object} +// * +// * Tht request data object +// * +// * ##### ctx.params.page {Number} +// * +// * Page number, `GET /api/users?page=10` => `10` +// * +// * ##### ctx.params.per_page {Number} +// * +// * The number of every page, `GET /api/users?per_page=20` => `20` +// */ +// params: any; + +// /** +// * @see Request#query +// */ +// query: PlainObject; + +// /** +// * @see Request#queries +// */ +// queries: PlainObject; + +// /** +// * @see Request#accept +// */ +// accept: accepts.Accepts; + +// /** +// * @see Request#acceptJSON +// */ +// acceptJSON: boolean; + +// /** +// * @see Request#ip +// */ +// ip: string; + +// /** +// * @see Response#realStatus +// */ +// realStatus: number; + +// /** +// * Set the ctx.body.data value +// * +// * @member {Object} Context#data= +// * @example +// * ```js +// * ctx.data = { +// * id: 1, +// * name: 'fengmk2' +// * }; +// * ``` +// * +// * will get responce +// * +// * ```js +// * HTTP/1.1 200 OK +// * +// * { +// * "data": { +// * "id": 1, +// * "name": "fengmk2" +// * } +// * } +// * ``` +// */ +// data: any; + +// /** +// * set ctx.body.meta value +// * +// * @example +// * ```js +// * ctx.meta = { +// * count: 100 +// * }; +// * ``` +// * will get responce +// * +// * ```js +// * HTTP/1.1 200 OK +// * +// * { +// * "meta": { +// * "count": 100 +// * } +// * } +// * ``` +// */ +// meta: any; + +// /** +// * locals is an object for view, you can use `app.locals` and `ctx.locals` to set variables, +// * which will be used as data when view is rendering. +// * The difference between `app.locals` and `ctx.locals` is the context level, `app.locals` is global level, and `ctx.locals` is request level. when you get `ctx.locals`, it will merge `app.locals`. +// * +// * when you set locals, only object is available +// * +// * ```js +// * this.locals = { +// * a: 1 +// * }; +// * this.locals = { +// * b: 1 +// * }; +// * this.locals.c = 1; +// * console.log(this.locals); +// * { +// * a: 1, +// * b: 1, +// * c: 1, +// * }; +// * ``` +// * +// * `ctx.locals` has cache, it only merges `app.locals` once in one request. +// * +// * @member {Object} Context#locals +// */ +// locals: IApplicationLocals & IContextLocals; + +// /** +// * alias to {@link locals}, compatible with koa that use this variable +// */ +// state: any; + +// /** +// * Logger for Application, wrapping app.coreLogger with context infomation +// * +// * @member {ContextLogger} Context#logger +// * @since 1.0.0 +// * @example +// * ```js +// * this.logger.info('some request data: %j', this.request.body); +// * this.logger.warn('WARNING!!!!'); +// * ``` +// */ +// logger: EggLogger; + +// /** +// * Get logger by name, it's equal to app.loggers['name'], but you can extend it with your own logical +// */ +// getLogger(name: string): EggLogger; + +// /** +// * Request start time +// */ +// starttime: number; + +// /** +// * Request start timer using `performance.now()` +// */ +// performanceStarttime?: number; + +// /** +// * http request helper base on httpclient, it will auto save httpclient log. +// * Keep the same api with httpclient.request(url, args). +// * See https://github.com/node-modules/urllib#api-doc for more details. +// */ +// curl: EggHttpClient['request']; + +// __(key: string, ...values: string[]): string; +// gettext(key: string, ...values: string[]): string; + +// /** +// * get upload file stream +// * @example +// * ```js +// * const stream = await this.getFileStream(); +// * // get other fields +// * console.log(stream.fields); +// * ``` +// * @function Context#getFileStream +// * @param {Object} options +// * @return {ReadStream} stream +// * @since 1.0.0 +// */ +// getFileStream(options?: GetFileStreamOptions): Promise; + +// /** +// * @see Responce.redirect +// */ +// redirect(url: string, alt?: string): void; + +// httpclient: EggContextHttpClient; +// } + +// export interface IContextLocals extends PlainObject { } + +// export class Controller extends BaseContextClass { } + +// export class Service extends BaseContextClass { } + +// export class Subscription extends BaseContextClass { } + +// /** +// * The empty interface `IService` is a placeholder, for egg +// * to auto injection service to ctx.service +// * +// * @example +// * +// * import { Service } from 'egg'; +// * class FooService extends Service { +// * async bar() {} +// * } +// * +// * declare module 'egg' { +// * export interface IService { +// * foo: FooService; +// * } +// * } +// * +// * Now I can get ctx.service.foo at controller and other service file. +// */ +// export interface IService extends PlainObject { } // tslint:disable-line + +// export interface IController extends PlainObject { } // tslint:disable-line + +// export interface IMiddleware extends PlainObject { } // tslint:disable-line + +// export interface IHelper extends PlainObject, BaseContextClass { +// /** +// * Generate URL path(without host) for route. Takes the route name and a map of named params. +// * @function Helper#pathFor +// * @param {String} name - Router Name +// * @param {Object} params - Other params +// * +// * @example +// * ```js +// * app.get('home', '/index.htm', 'home.index'); +// * ctx.helper.pathFor('home', { by: 'recent', limit: 20 }) +// * => /index.htm?by=recent&limit=20 +// * ``` +// * @return {String} url path(without host) +// */ +// pathFor(name: string, params?: PlainObject): string; + +// /** +// * Generate full URL(with host) for route. Takes the route name and a map of named params. +// * @function Helper#urlFor +// * @param {String} name - Router name +// * @param {Object} params - Other params +// * @example +// * ```js +// * app.get('home', '/index.htm', 'home.index'); +// * ctx.helper.urlFor('home', { by: 'recent', limit: 20 }) +// * => http://127.0.0.1:7001/index.htm?by=recent&limit=20 +// * ``` +// * @return {String} full url(with host) +// */ +// urlFor(name: string, params?: PlainObject): string; +// } + +// // egg env type +// export type EggEnvType = 'local' | 'unittest' | 'prod' | string; + +// /** +// * plugin config item interface +// */ +// export interface IEggPluginItem { +// env?: EggEnvType[]; +// path?: string; +// package?: string; +// enable?: boolean; +// } + +// export type EggPluginItem = IEggPluginItem | boolean; + +// /** +// * build-in plugin list +// */ +// export interface EggPlugin { +// [key: string]: EggPluginItem | undefined; +// onerror?: EggPluginItem; +// session?: EggPluginItem; +// i18n?: EggPluginItem; +// watcher?: EggPluginItem; +// multipart?: EggPluginItem; +// security?: EggPluginItem; +// development?: EggPluginItem; +// logrotator?: EggPluginItem; +// schedule?: EggPluginItem; +// static?: EggPluginItem; +// jsonp?: EggPluginItem; +// view?: EggPluginItem; +// } + +// /** +// * Singleton instance in Agent Worker, extend {@link EggApplication} +// */ +// export class Agent extends EggApplication { +// } + +// export interface ClusterOptions { +// /** specify framework that can be absolute path or npm package */ +// framework?: string; +// /** directory of application, default to `process.cwd()` */ +// baseDir?: string; +// /** customized plugins, for unittest */ +// plugins?: object | null; +// /** numbers of app workers, default to `os.cpus().length` */ +// workers?: number; +// /** listening port, default to 7001(http) or 8443(https) */ +// port?: number; +// /** https or not */ +// https?: boolean; +// /** ssl key */ +// key?: string; +// /** ssl cert */ +// cert?: string; +// [prop: string]: any; +// } + +// export function startCluster(options: ClusterOptions, callback: (...args: any[]) => any): void; + +// export interface StartOptions { +// /** specify framework that can be absolute path or npm package */ +// framework?: string; +// /** directory of application, default to `process.cwd()` */ +// baseDir?: string; +// /** ignore single process mode warning */ +// ignoreWarning?: boolean; +// } + +// export function start(options?: StartOptions): Promise; + +// /** +// * Powerful Partial, Support adding ? modifier to a mapped property in deep level +// * @example +// * import { PowerPartial, EggAppConfig } from 'egg'; +// * +// * // { view: { defaultEngines: string } } => { view?: { defaultEngines?: string } } +// * type EggConfig = PowerPartial +// */ +// export type PowerPartial = { +// [U in keyof T]?: T[U] extends object +// ? PowerPartial +// : T[U] +// }; + +// // send data can be number|string|boolean|object but not Set|Map +// export interface Messenger extends EventEmitter { +// /** +// * broadcast to all agent/app processes including itself +// */ +// broadcast(action: string, data: any): void; + +// /** +// * send to agent from the app, +// * send to an random app from the agent +// */ +// sendRandom(action: string, data: any): void; + +// /** +// * send to specified process +// */ +// sendTo(pid: number, action: string, data: any): void; + +// /** +// * send to agent from the app, +// * send to itself from the agent +// */ +// sendToAgent(action: string, data: any): void; + +// /** +// * send to all app including itself from the app, +// * send to all app from the agent +// */ +// sendToApp(action: string, data: any): void; +// } + +// // compatible +// export interface EggLoaderOptions extends CoreLoaderOptions { } +// export interface EggLoader extends CoreLoader { } + +// /** +// * App worker process Loader, will load plugins +// * @see https://github.com/eggjs/egg-core +// */ +// export class AppWorkerLoader extends CoreLoader { +// loadConfig(): void; +// load(): void; +// } + +// /** +// * Agent worker process loader +// * @see https://github.com/eggjs/egg-loader +// */ +// export class AgentWorkerLoader extends CoreLoader { +// loadConfig(): void; +// load(): void; +// } + +// export interface IBoot { +// /** +// * Ready to call configDidLoad, +// * Config, plugin files are referred, +// * this is the last chance to modify the config. +// */ +// configWillLoad?(): void; + +// /** +// * Config, plugin files have loaded +// */ +// configDidLoad?(): void; + +// /** +// * All files have loaded, start plugin here +// */ +// didLoad?(): Promise; + +// /** +// * All plugins have started, can do some thing before app ready +// */ +// willReady?(): Promise; + +// /** +// * Worker is ready, can do some things, +// * don't need to block the app boot +// */ +// didReady?(): Promise; + +// /** +// * Server is listening +// */ +// serverDidReady?(): Promise; + +// /** +// * Do some thing before app close +// */ +// beforeClose?(): Promise; +// } + +// export interface Singleton { +// get(id: string): T; +// } +// } diff --git a/package.json b/package.json index ad74f66a1b..2227e3cf45 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "coffee": "5", "cross-env": "7", "egg-bin": "beta", - "egg-mock": "beta", + "@eggjs/mock": "beta", "egg-plugin-puml": "^2.4.0", "egg-tracer": "^2.1.0", "egg-view-nunjucks": "^2.3.0", @@ -76,6 +76,7 @@ "eslint-config-egg": "14", "formstream": "^1.5.1", "koa-static": "^5.0.0", + "mm": "^3.4.0", "pedding": "^1.1.0", "prettier": "^2.7.1", "runscript": "^2.0.1", @@ -107,6 +108,9 @@ "url": "git://github.com/eggjs/egg.git" }, "license": "MIT", + "tnpm": { + "mode": "npm" + }, "egg": { "framework": true, "exports": { diff --git a/site/docs/advanced/framework.md b/site/docs/advanced/framework.md index 3eb3e8c91f..3ffeaf818e 100644 --- a/site/docs/advanced/framework.md +++ b/site/docs/advanced/framework.md @@ -6,7 +6,7 @@ order: 3 If your team have met with these scenarios: - Each project contains the same configuration files that need to be copied every time, such as `gulpfile.js`, `webpack.config.js`. -- Each project has similiar dependencies. +- Each project has similar dependencies. - It's difficult to synchronize those projects based on the same configurations like those mentioned above once they have been optimized? If your team needs: @@ -16,7 +16,7 @@ If your team needs: - a unified [deployment plan](../core/deployment.md) keeping developers concentrate on code without paying attention to deployment details of connecting the framework and platforms. - a unified code style to decrease code's repetition and optimize code's appearance, which is important for a enterprise level framework. -To satisfy these demands, Egg endows developers with the capacity of `customazing a framework`. It is just an abstract layer, which can be constructed to a higher level framework, supporting inheritance of unlimited times. Futhermore, Egg apply a quantity of coding conventions based on Koa. +To satisfy these demands, Egg endows developers with the capacity of `customizing a framework`. It is just an abstract layer, which can be constructed to a higher level framework, supporting inheritance of unlimited times. Furthermore, Egg apply a quantity of coding conventions based on Koa. Therefore, a uniform spec can be applied on projects in which the differentiation fulfilled in plugins. And the best practice summed from those projects can be continuously extracted from these plugins to the framework, which is available to other projects by just updating the dependencies' versions. @@ -30,7 +30,7 @@ They both are inherited from [EggCore](https://github.com/eggjs/egg-core), and A We could regard EggCore as the advanced version of Koa Application, which integrates built-in features such as [Loader](./loader.md)、[Router](../basics/router.md) and asynchronous launch. -``` +```bash Koa Application ^ EggCore @@ -92,7 +92,7 @@ To customize framework, Loader is required and has to be inherited from Egg Load If we consider a framework as a class, then Egg framework is the base class,and implementing a framework demands to implement entire APIs of Egg. -```js +```bash // package.json { "name": "yadan", @@ -122,7 +122,7 @@ module.exports = Object.assign(egg, { }); ``` -The name of framework, default as `egg`, is a indispensable option to launch an application, set by `egg.framwork` of `package.json`, then Loader loads the exported app of a module named it. +The name of framework, default as `egg`, is a indispensable option to launch an application, set by `egg.framework` of `package.json`, then Loader loads the exported app of a module named it. ```json { @@ -139,7 +139,7 @@ As a loadUnit of framework, yadan is going to load specific directories and file ### Principle of Framework Extension -The path of framework is set as a varible named as `Symbol.for('egg#eggPath')` to expose itself to Loader. Why? It seems that the simplest way is to pass a param to the constructor. The reason is to expose those paths of each level of inherited frameworks and reserve their sequences. Since Egg is a framework capable of unlimited inheritance, each layer has to designate their own eggPath so that all the eggPaths are accessiable through the prototype chain. +The path of framework is set as a variable named as `Symbol.for('egg#eggPath')` to expose itself to Loader. Why? It seems that the simplest way is to pass a param to the constructor. The reason is to expose those paths of each level of inherited frameworks and reserve their sequences. Since Egg is a framework capable of unlimited inheritance, each layer has to designate their own eggPath so that all the eggPaths are accessible through the prototype chain. Given a triple-layer framework: department level > enterprise level > Egg @@ -173,7 +173,7 @@ These code are pseudocode to elaborate the framework's loading process, and we h ### Custom Agent -Egg's mutilprocess model is composed of Application and Agent. Therefore Agent, another fundamental class similiar to Application, is also required to be implemented. +Egg's multiprocess model is composed of Application and Agent. Therefore Agent, another fundamental class similar to Application, is also required to be implemented. ```js // lib/framework.js @@ -207,12 +207,13 @@ module.exports = Object.assign(egg, { Loader, the core of the launch process, is capable of loading data code, adjusting loading orders or even strengthen regulation of code. -As the same as Egg-Path, Loader exposes itself at `Symbol.for('egg#loader')` to ensuer it's accessibility on prototype chain. +As the same as Egg-Path, Loader exposes itself at `Symbol.for('egg#loader')` to ensure it's accessibility on prototype chain. ```js // lib/framework.js const path = require('path'); const egg = require('egg'); + const EGG_PATH = Symbol.for('egg#eggPath'); class YadanAppWorkerLoader extends egg.AppWorkerLoader { @@ -236,16 +237,16 @@ class Application extends egg.Application { // rewrite Egg's Application module.exports = Object.assign(egg, { Application, - // custom Loader, a dependence of the high level frameword, needs to be exported. + // custom Loader, a dependence of the high level framework, needs to be exported. AppWorkerLoader: YadanAppWorkerLoader, }); ``` -AgentworkerLoader is not going to be described because of it's similarity of AppWorkerLoader, but be aware of it's located at `agent.js` instand of `app.js`. +AgentWorkerLoader is not going to be described because of it's similarity of AppWorkerLoader, but be aware of it's located at `agent.js` instead of `app.js`. ## The principle of Launch -Many descriptions of launch process are scattered at [Mutilprocess Model](../core/cluster-and-ipc.md), [Loader](./loader.md) and [Plugin](./plugin.md), and here is a summarization. +Many descriptions of launch process are scattered at [Multiprocess Model](../core/cluster-and-ipc.md), [Loader](./loader.md) and [Plugin](./plugin.md), and here is a summarization. - `startCluster` is invoked with `baseDir` and `framework`, then Master process is launched. - Master forks a new process as Agent Worker @@ -253,22 +254,22 @@ Many descriptions of launch process are scattered at [Mutilprocess Model](../cor - Agent finds out the AgentWorkerLoader and then starts to load - use AgentWorkerLoader to load Worker synchronously in the sequence of Plugin Config, Extend, `agent.js` and other files. - The initiation of `agent.js` is able to be customized, and it supports asynchronous launch after which it notifies Master and invoke the function passed to `beforeStart`. -- After recieving the message that Agent Worker is launched,Master forks App Workers by cluster. - - App Workers are mutilple identical processes launched simultaneously - - App Worker is instantiated, which is similiar to Agent inherited Application class of framework loaded from framework path. +- After receiving the message that Agent Worker is launched,Master forks App Workers by cluster. + - App Workers are multiple identical processes launched simultaneously + - App Worker is instantiated, which is similar to Agent inherited Application class of framework loaded from framework path. - The same as Agent, Loading process of Application starts with AppWorkerLoader which loads files in the same order and finally informed Master. -- After informed of luanching successfully of each App Worker, Master is finally functioning. +- After informed of launching successfully of each App Worker, Master is finally functioning. ## Framework Testing -You'd better read [unittest](../core/unittest.md) first, which is similiar to framework testing in a quantity of situations. +You'd better read [unittest](../core/unittest.md) first, which is similar to framework testing in a quantity of situations. ### Initiation Here are some differences between initiation of frameworks. ```js -const mock = require('egg-mock'); +const mock = require('@eggjs/mock'); describe('test/index.test.js', () => { let app; before(() => { @@ -293,15 +294,16 @@ describe('test/index.test.js', () => { - Different from application testing, framework testing tests framework code instead of application code, so that baseDir varies for the propose of testing kinds of applications. - BaseDir is potentially considered to be under the path of `test/fixtures`, otherwise it should be absolute paths. - The `framework` option is indispensable, which could be a absolute path or `true` meaning the path of the framework to be current directory. -- The use of the app should wait for the `ready` event in `before` hook, or some of the APIs is not avaiable. +- The use of the app should wait for the `ready` event in `before` hook, or some of the APIs is not available. - Do not forget to invoke `app.close()` after testing, which could arouse the exhausting of fds, caused by unclosed log files. ### Cache -`mm.app` enables cache as default, which means new envoriment setting would not work once loaded. +`mm.app` enables cache as default, which means new environment setting would not work once loaded. ```js -const mock = require('egg-mock'); +const mock = require('@eggjs/mock'); + describe('/test/index.test.js', () => { let app; afterEach(() => app.close()); @@ -327,15 +329,16 @@ describe('/test/index.test.js', () => { }); ``` -### Multipleprocess Testing +### Multiprocess Testing -Mutilprocess is rarely tested because of the high cost and the unavailability of API level's mock, meanwhile, processes have a slow start or even timeout, but it still remains the most effective way of testing multiprocess model. +Multiprocess is rarely tested because of the high cost and the unavailability of API level's mock, meanwhile, processes have a slow start or even timeout, but it still remains the most effective way of testing multiprocess model. The option of `mock.cluster` have no difference with `mm.app` while their APIs are totally distinct, however, SuperTest still works. ```js -const mock = require('egg-mock'); -describe('/test/index.test.js', () => { +const mock = require('@eggjs/mock'); + +describe('test/index.test.js', () => { let app; before(() => { app = mock.cluster({ @@ -346,16 +349,20 @@ describe('/test/index.test.js', () => { }); after(() => app.close()); afterEach(mock.restore); + it('should success', () => { - return app.httpRequest().get('/').expect(200); + return app.httpRequest() + .get('/') + .expect(200); }); }); ``` -Tests of `stdout/stderr` are also avaiable, since `mm.cluster` is based on [coffee](https://github.com/popomore/coffee) in which multiprocess testing is supported. +Tests of `stdout/stderr` are also available, since `mm.cluster` is based on [coffee](https://github.com/popomore/coffee) in which multiprocess testing is supported. ```js -const mock = require('egg-mock'); +const mock = require('@eggjs/mock'); + describe('/test/index.test.js', () => { let app; before(() => { @@ -366,6 +373,7 @@ describe('/test/index.test.js', () => { return app.ready(); }); after(() => app.close()); + it('should get `started`', () => { // set the expectation of console app.expect('stdout', /started/); diff --git a/site/docs/advanced/framework.zh-CN.md b/site/docs/advanced/framework.zh-CN.md index 99dc71efac..69c957ccf0 100644 --- a/site/docs/advanced/framework.zh-CN.md +++ b/site/docs/advanced/framework.zh-CN.md @@ -259,6 +259,7 @@ AgentWorkerLoader 的扩展也类似,这里不再赘述。AgentWorkerLoader - 单个 App Worker 通过 framework 找到框架目录,实例化该框架的 Application 类。 - Application 根据 AppWorkerLoader 开始加载,加载顺序类似,会异步等待完成后通知 Master 启动完成。 4. Master 在等到所有 App Worker 发来的启动成功消息后,完成启动,开始对外提供服务。 + ## 框架测试 在看下文之前,请先查看[单元测试章节](../core/unittest.md)。框架测试的大部分使用场景和应用类似。 @@ -268,7 +269,8 @@ AgentWorkerLoader 的扩展也类似,这里不再赘述。AgentWorkerLoader 框架的初始化方式有一定差异。 ```js -const mock = require('egg-mock'); +const mock = require('@eggjs/mock'); + describe('test/index.test.js', () => { let app; before(() => { @@ -301,8 +303,9 @@ describe('test/index.test.js', () => { 在测试多环境场景需要使用到 cache 参数,因为 `mock.app` 默认有缓存,当第一次加载后再次加载会直接读取缓存,那么设置的环境也不会生效。 ```js -const mock = require('egg-mock'); -describe('/test/index.test.js', () => { +const mock = require('@eggjs/mock'); + +describe('test/index.test.js', () => { let app; afterEach(() => app.close()); @@ -334,8 +337,9 @@ describe('/test/index.test.js', () => { 多进程测试和 `mock.app` 参数一致,但 app 的 API 完全不同。不过,SuperTest 依然可用。 ```js -const mock = require('egg-mock'); -describe('/test/index.test.js', () => { +const mock = require('@eggjs/mock'); + +describe('test/index.test.js', () => { let app; before(() => { app = mock.cluster({ @@ -346,8 +350,11 @@ describe('/test/index.test.js', () => { }); after(() => app.close()); afterEach(mock.restore); + it('should success', () => { - return app.httpRequest().get('/').expect(200); + return app.httpRequest() + .get('/') + .expect(200); }); }); ``` @@ -355,8 +362,9 @@ describe('/test/index.test.js', () => { 多进程测试还可以测试 stdout/stderr,因为 `mock.cluster` 是基于 [coffee](https://github.com/popomore/coffee) 扩展的,可进行进程测试。 ```js -const mock = require('egg-mock'); -describe('/test/index.test.js', () => { +const mock = require('@eggjs/mock'); + +describe('test/index.test.js', () => { let app; before(() => { app = mock.cluster({ @@ -366,6 +374,7 @@ describe('/test/index.test.js', () => { return app.ready(); }); after(() => app.close()); + it('should get `started`', () => { // 判断终端输出 app.expect('stdout', /started/); diff --git a/site/docs/basics/schedule.md b/site/docs/basics/schedule.md index 734f72bf9c..6e9b84d111 100644 --- a/site/docs/basics/schedule.md +++ b/site/docs/basics/schedule.md @@ -172,7 +172,7 @@ There are some scenarios we may need to manually execute scheduled tasks, for ex - Executing scheduled tasks manually for more elegant unit testing of scheduled tasks. ```js -const mm = require('egg-mock'); +const mm = require('@eggjs/mock'); const assert = require('assert'); it('should schedule work fine', async () => { diff --git a/site/docs/basics/schedule.zh-CN.md b/site/docs/basics/schedule.zh-CN.md index 40f9946bd1..0234ce3b4d 100644 --- a/site/docs/basics/schedule.zh-CN.md +++ b/site/docs/basics/schedule.zh-CN.md @@ -170,7 +170,7 @@ module.exports = (app) => { - 手动执行定时任务可以更优雅地编写定时任务的单元测试。 ```js -const mm = require('egg-mock'); +const mm = require('@eggjs/mock'); const assert = require('assert'); it('should schedule work fine', async () => { @@ -218,4 +218,4 @@ module.exports = (agent) => { - `this.schedule` - 定时任务的属性,所有任务默认支持的 `disable` 属性,以及其他自定义配置的解析。 - `this.sendOne(...args)` - 随机通知某个 worker 执行 task,`args` 会传递给 `subscribe(...args)` 或 `task(ctx, ...args)` 方法。 -- `this.sendAll(...args)` - 通知所有的 worker 执行 task。 \ No newline at end of file +- `this.sendAll(...args)` - 通知所有的 worker 执行 task。 diff --git a/site/docs/core/unittest.md b/site/docs/core/unittest.md index 6ee85db358..aeaff19ca9 100644 --- a/site/docs/core/unittest.md +++ b/site/docs/core/unittest.md @@ -136,7 +136,7 @@ We can easily create an app instance with Mocha's `before` hook through egg-mock ```js // test/controller/home.test.js const assert = require('assert'); -const mock = require('egg-mock'); +const mock = require('@eggjs/mock'); describe('test/controller/home.test.js', () => { let app; diff --git a/site/docs/core/unittest.zh-CN.md b/site/docs/core/unittest.zh-CN.md index 70c985ec9d..881885ddad 100644 --- a/site/docs/core/unittest.zh-CN.md +++ b/site/docs/core/unittest.zh-CN.md @@ -139,7 +139,7 @@ npm test ```javascript // test/controller/home.test.js const assert = require('assert'); -const mock = require('egg-mock'); +const mock = require('@eggjs/mock'); describe('test/controller/home.test.js', () => { let app; diff --git a/src/lib/core/messenger/IMessenger.ts b/src/lib/core/messenger/IMessenger.ts index 8f4ac5d757..9aa752e7f3 100644 --- a/src/lib/core/messenger/IMessenger.ts +++ b/src/lib/core/messenger/IMessenger.ts @@ -11,12 +11,12 @@ export interface IMessenger extends EventEmitter { /** * send message to the specified process - * @param {String} pid - the process id of the receiver + * @param {String} workerId - the workerId of the receiver * @param {String} action - message key * @param {Object} data - message value * @return {Messenger} this */ - sendTo(pid: string, action: string, data?: unknown): IMessenger; + sendTo(workerId: string, action: string, data?: unknown): IMessenger; /** * send message to one app worker by random diff --git a/src/lib/core/messenger/index.ts b/src/lib/core/messenger/index.ts index df8784a83b..e2473a2b0b 100644 --- a/src/lib/core/messenger/index.ts +++ b/src/lib/core/messenger/index.ts @@ -11,5 +11,5 @@ export type { IMessenger } from './IMessenger.js'; export function create(egg: EggApplicationCore): IMessenger { return egg.options.mode === 'single' ? new LocalMessenger(egg) - : new IPCMessenger(); + : new IPCMessenger(egg); } diff --git a/src/lib/core/messenger/ipc.ts b/src/lib/core/messenger/ipc.ts index 6052fb4df0..2e92ab53c8 100644 --- a/src/lib/core/messenger/ipc.ts +++ b/src/lib/core/messenger/ipc.ts @@ -3,24 +3,28 @@ import { debuglog } from 'node:util'; import workerThreads from 'node:worker_threads'; import { sendmessage } from 'sendmessage'; import type { IMessenger } from './IMessenger.js'; +import type { EggApplicationCore } from '../../egg.js'; -const debug = debuglog('egg:lib:core:messenger:ipc'); +const debug = debuglog('egg/lib/core/messenger/ipc'); /** * Communication between app worker and agent worker by IPC channel */ export class Messenger extends EventEmitter implements IMessenger { readonly pid: string; + readonly egg: EggApplicationCore; opids: string[] = []; - constructor() { + constructor(egg: EggApplicationCore) { super(); this.pid = String(process.pid); + this.egg = egg; // pids of agent or app managed by master // - retrieve app worker pids when it's an agent worker // - retrieve agent worker pids when it's an app worker - this.on('egg-pids', pids => { - this.opids = pids; + this.on('egg-pids', workerIds => { + debug('[%s:%s] got egg-pids %j', this.egg.type, this.pid, workerIds); + this.opids = workerIds.map((workerId: number) => String(workerId)); }); this.onMessage = this.onMessage.bind(this); process.on('message', this.onMessage); @@ -36,7 +40,7 @@ export class Messenger extends EventEmitter implements IMessenger { * @return {Messenger} this */ broadcast(action: string, data?: unknown): Messenger { - debug('[%s] broadcast %s with %j', this.pid, action, data); + debug('[%s:%s] broadcast %s with %j', this.egg.type, this.pid, action, data); this.send(action, data, 'app'); this.send(action, data, 'agent'); return this; @@ -44,21 +48,21 @@ export class Messenger extends EventEmitter implements IMessenger { /** * send message to the specified process - * @param {String} pid - the process id of the receiver + * @param {String} workerId - the workerId of the receiver * @param {String} action - message key * @param {Object} data - message value * @return {Messenger} this */ - sendTo(pid: string, action: string, data?: unknown): Messenger { - debug('[%s] send %s with %j to %s', this.pid, action, data, pid); + sendTo(workerId: string, action: string, data?: unknown): Messenger { + debug('[%s:%s] send %s with %j to workerId:%s', this.egg.type, this.pid, action, data, workerId); sendmessage(process, { action, data, /** * @deprecated Keep compatible, please use receiverWorkerId instead */ - receiverPid: String(pid), - receiverWorkerId: String(pid), + receiverPid: String(workerId), + receiverWorkerId: String(workerId), }); return this; } @@ -73,11 +77,12 @@ export class Messenger extends EventEmitter implements IMessenger { */ sendRandom(action: string, data?: unknown): Messenger { if (this.opids.length === 0) { + debug('[%s:%s] no pids, ignore sendRandom %s with %j', this.egg.type, this.pid, action, data); return this; } const index = Math.floor(Math.random() * this.opids.length); - const pid = this.opids[index]; - this.sendTo(String(pid), action, data); + const workerId = this.opids[index]; + this.sendTo(workerId, action, data); return this; } @@ -88,7 +93,7 @@ export class Messenger extends EventEmitter implements IMessenger { * @return {Messenger} this */ sendToApp(action: string, data?: unknown): Messenger { - debug('[%s] send %s with %j to all app', this.pid, action, data); + debug('[%s:%s] send %s with %j to all app', this.egg.type, this.pid, action, data); this.send(action, data, 'app'); return this; } @@ -100,7 +105,7 @@ export class Messenger extends EventEmitter implements IMessenger { * @return {Messenger} this */ sendToAgent(action: string, data?: unknown): Messenger { - debug('[%s] send %s with %j to all agent', this.pid, action, data); + debug('[%s:%s] send %s with %j to all agent', this.egg.type, this.pid, action, data); this.send(action, data, 'agent'); return this; } @@ -122,9 +127,11 @@ export class Messenger extends EventEmitter implements IMessenger { onMessage(message: any) { if (typeof message?.action === 'string') { - debug('[%s] got message %s with %j, receiverWorkerId: %s', - this.pid, message.action, message.data, message.receiverWorkerId ?? message.receiverPid); + debug('[%s:%s] got message %s with %j, receiverWorkerId: %s', + this.egg.type, this.pid, message.action, message.data, message.receiverWorkerId ?? message.receiverPid); this.emit(message.action, message.data); + } else { + debug('[%s:%s] got an invalid message %j', this.egg.type, this.pid, message); } } diff --git a/src/lib/core/messenger/local.ts b/src/lib/core/messenger/local.ts index 6bbf228b04..2f9a93a4d7 100644 --- a/src/lib/core/messenger/local.ts +++ b/src/lib/core/messenger/local.ts @@ -3,7 +3,7 @@ import EventEmitter from 'node:events'; import type { IMessenger } from './IMessenger.js'; import type { EggApplicationCore } from '../../egg.js'; -const debug = debuglog('egg:lib:core:messenger:local'); +const debug = debuglog('egg/lib/core/messenger/local'); /** * Communication between app worker and agent worker with EventEmitter @@ -25,7 +25,7 @@ export class Messenger extends EventEmitter implements IMessenger { * @return {Messenger} this */ broadcast(action: string, data?: unknown): Messenger { - debug('[%s] broadcast %s with %j', this.pid, action, data); + debug('[%s:%s] broadcast %s with %j', this.egg.type, this.pid, action, data); this.send(action, data, 'both'); return this; } @@ -34,14 +34,14 @@ export class Messenger extends EventEmitter implements IMessenger { * send message to the specified process * Notice: in single process mode, it only can send to self process, * and it will send to both agent and app's messengers. - * @param {String} pid - the process id of the receiver + * @param {String} workerId - the workerId of the receiver * @param {String} action - message key * @param {Object} data - message value * @return {Messenger} this */ - sendTo(pid: string, action: string, data?: unknown): Messenger { - debug('[%s] send %s with %j to %s', this.pid, action, data, pid); - if (String(pid) !== this.pid) { + sendTo(workerId: string, action: string, data?: unknown): Messenger { + debug('[%s:%s] send %s with %j to %s', this.egg.type, this.pid, action, data, workerId); + if (String(workerId) !== this.pid) { return this; } this.send(action, data, 'both'); @@ -58,7 +58,7 @@ export class Messenger extends EventEmitter implements IMessenger { * @return {Messenger} this */ sendRandom(action: string, data?: unknown): Messenger { - debug('[%s] send %s with %j to opposite', this.pid, action, data); + debug('[%s:%s] send %s with %j to opposite', this.egg.type, this.pid, action, data); this.send(action, data, 'opposite'); return this; } @@ -70,7 +70,7 @@ export class Messenger extends EventEmitter implements IMessenger { * @return {Messenger} this */ sendToApp(action: string, data?: unknown): Messenger { - debug('[%s] send %s with %j to all app', this.pid, action, data); + debug('[%s:%s] send %s with %j to all app', this.egg.type, this.pid, action, data); this.send(action, data, 'application'); return this; } @@ -82,7 +82,7 @@ export class Messenger extends EventEmitter implements IMessenger { * @return {Messenger} this */ sendToAgent(action: string, data?: unknown): Messenger { - debug('[%s] send %s with %j to all agent', this.pid, action, data); + debug('[%s:%s] send %s with %j to all agent', this.egg.type, this.pid, action, data); this.send(action, data, 'agent'); return this; } @@ -130,8 +130,10 @@ export class Messenger extends EventEmitter implements IMessenger { onMessage(message: any) { if (typeof message?.action === 'string') { - debug('[%s] got message %s with %j', this.pid, message.action, message.data); + debug('[%s:%s] got message %s with %j', this.egg.type, this.pid, message.action, message.data); this.emit(message.action, message.data); + } else { + debug('[%s:%s] got an invalid message %j', this.egg.type, this.pid, message); } } diff --git a/test/app/extend/context.test.ts b/test/app/extend/context.test.ts index bdb2c87ba3..ef9823f66d 100644 --- a/test/app/extend/context.test.ts +++ b/test/app/extend/context.test.ts @@ -312,7 +312,7 @@ describe('test/app/extend/context.test.ts', () => { }); describe('ctx.runInBackground(scope) with single process mode', () => { - // ctx.runInBackground with egg-mock are override + // ctx.runInBackground with @eggjs/mock are override // single process mode will use the original ctx.runInBackground let app: MockSingleProcessApplication; before(async () => { @@ -483,7 +483,7 @@ describe('test/app/extend/context.test.ts', () => { it('should work with setter app.router', () => { const ctx = app.mockContext(); - ctx.router = 'router'; + (ctx as any).router = 'router'; assert.equal(ctx.router, 'router'); }); }); diff --git a/test/app/extend/response.test.js b/test/app/extend/response.test.js deleted file mode 100644 index 6c38dc21c2..0000000000 --- a/test/app/extend/response.test.js +++ /dev/null @@ -1,87 +0,0 @@ -'use strict'; - -const assert = require('assert'); -const mm = require('egg-mock'); -const utils = require('../../utils'); - -describe('test/app/extend/response.test.js', () => { - afterEach(mm.restore); - - describe('length and type', () => { - let app; - before(() => { - app = utils.app('apps/response'); - return app.ready(); - }); - after(() => app.close()); - - it('should get lower case header', () => { - return app.httpRequest() - .get('/') - .expect(200) - .expect(res => { - assert(res.res.rawHeaders.indexOf('content-type') >= 0); - assert(res.res.rawHeaders.indexOf('content-length') >= 0); - }); - }); - - it('should get body length', () => { - const ctx = app.mockContext(); - ctx.body = null; - ctx.response.remove('content-length'); - assert(ctx.response.length === undefined); - ctx.body = '1'; - ctx.response.remove('content-length'); - assert(ctx.response.length === 1); - ctx.body = Buffer.alloc(2); - ctx.response.remove('content-length'); - assert(ctx.response.length === 2); - ctx.body = {}; - ctx.response.remove('content-length'); - assert(ctx.response.length === 2); - // mock stream - ctx.body = { pipe() {} }; - ctx.response.remove('content-length'); - assert(ctx.response.length === undefined); - }); - }); - - describe('test on apps/demo', () => { - let app; - before(() => { - app = utils.app('apps/demo'); - return app.ready(); - }); - after(() => app.close()); - - describe('response.realStatus', () => { - it('should get from status ok', () => { - const ctx = app.mockContext(); - ctx.response.status = 200; - assert(ctx.response.realStatus === 200); - assert(ctx.realStatus === 200); - }); - - it('should get from realStatus ok', () => { - const ctx = app.mockContext(); - ctx.response.status = 302; - ctx.response.realStatus = 404; - assert(ctx.response.realStatus === 404); - assert(ctx.realStatus === 404); - }); - }); - - describe('response.type = type', () => { - it('should remove content-type when type is invalid', () => { - const ctx = app.mockContext(); - ctx.response.type = 'html'; - assert(ctx.response.header['content-type'] === 'text/html; charset=utf-8'); - assert(ctx.response.type === 'text/html'); - - ctx.response.type = 'html-ooooooxxx'; - assert(ctx.response.header['content-type'] === undefined); - assert(ctx.response.type === ''); - }); - }); - }); -}); diff --git a/test/app/extend/response.test.ts b/test/app/extend/response.test.ts new file mode 100644 index 0000000000..8318bf679d --- /dev/null +++ b/test/app/extend/response.test.ts @@ -0,0 +1,115 @@ +import { strict as assert } from 'node:assert'; +import { restore, MockApplication, createApp } from '../../utils.js'; + +describe('test/app/extend/response.test.ts', () => { + afterEach(restore); + + describe('length and type', () => { + let app: MockApplication; + before(() => { + app = createApp('apps/response'); + return app.ready(); + }); + after(() => app.close()); + + it('should get case sensitive header', () => { + return app.httpRequest() + .get('/') + .expect(200) + .expect((res: any) => { + assert.match(JSON.stringify(res.res.rawHeaders), /Content-Type/); + assert.match(JSON.stringify(res.res.rawHeaders), /Content-Length/); + }); + }); + + it('should get {} body', async () => { + const res = await app.httpRequest() + .get('/empty-json') + .expect(200); + assert.deepEqual(res.body, {}); + assert.equal(res.headers['content-length'], '2'); + assert.equal(res.headers['content-type'], 'application/json; charset=utf-8'); + }); + + it('should get body length', () => { + const ctx = app.mockContext(); + ctx.body = null; + const response = ctx.response; + response.remove('content-length'); + assert.equal(response.length, undefined); + + ctx.body = '1'; + response.remove('content-length'); + assert.equal(response.length, 1); + + ctx.body = Buffer.alloc(2); + response.remove('content-length'); + assert.equal(response.length, 2); + + ctx.body = { foo: 'bar' }; + response.remove('content-length'); + assert.equal(response.length, 13); + + ctx.body = '{}'; + response.remove('content-length'); + assert.equal(response.length, 2); + + ctx.body = {}; + response.remove('content-length'); + assert.equal(response.length, 2); + + // mock stream + // ctx.body = { pipe() {} }; + // response.remove('content-length'); + // assert.equal(response.length, undefined); + }); + }); + + describe('test on apps/demo', () => { + let app: MockApplication; + before(() => { + app = createApp('apps/demo'); + return app.ready(); + }); + after(() => app.close()); + + describe('response.realStatus', () => { + it('should get from status ok', () => { + const ctx = app.mockContext(); + ctx.response.status = 200; + assert(ctx.response.realStatus === 200); + assert(ctx.realStatus === 200); + }); + + it('should get from realStatus ok', () => { + const ctx = app.mockContext(); + ctx.response.status = 302; + ctx.response.realStatus = 404; + assert(ctx.response.realStatus === 404); + assert(ctx.realStatus === 404); + }); + }); + + describe('response.type = type', () => { + it('should remove content-type when type is invalid', () => { + let ctx = app.mockContext(); + ctx.response.type = 'html'; + assert.equal(ctx.response.header['content-type'], 'text/html; charset=utf-8'); + assert.equal(ctx.response.type, 'text/html'); + + ctx.response.type = 'xml'; + assert.equal(ctx.response.header['content-type'], 'application/xml'); + assert.equal(ctx.response.type, 'application/xml'); + + ctx = app.mockContext(); + ctx.response.type = 'html-ooooooxxx'; + assert.equal(ctx.response.header['content-type'], undefined); + assert.equal(ctx.response.type, ''); + + ctx.response.type = 'html'; + assert.equal(ctx.response.header['content-type'], 'text/html; charset=utf-8'); + assert.equal(ctx.response.type, 'text/html'); + }); + }); + }); +}); diff --git a/test/app/middleware/meta.test.ts b/test/app/middleware/meta.test.ts index 69f370dced..d877a82048 100644 --- a/test/app/middleware/meta.test.ts +++ b/test/app/middleware/meta.test.ts @@ -73,7 +73,7 @@ describe('test/app/middleware/meta.test.ts', () => { return app.httpRequest() .get('/') .expect('X-Readtime', /\d+/) - .expect(res => assert(!res.headers['keep-alive'])) + .expect((res: any) => assert(!res.headers['keep-alive'])) .expect(200); }); diff --git a/test/bench/server.js b/test/bench/server.js index d2d9508dc4..f9cd663b24 100644 --- a/test/bench/server.js +++ b/test/bench/server.js @@ -1,6 +1,6 @@ const http = require('http'); const path = require('path'); -const mock = require('egg-mock'); +const mock = require('@eggjs/mock'); const appName = process.argv[2]; diff --git a/test/fixtures/apps/app-ts-type-check/xiandan.d.ts b/test/fixtures/apps/app-ts-type-check/xiandan.d.ts index 8bd94303c8..65e7d5819a 100644 --- a/test/fixtures/apps/app-ts-type-check/xiandan.d.ts +++ b/test/fixtures/apps/app-ts-type-check/xiandan.d.ts @@ -1,23 +1,23 @@ -import * as Egg from 'yadan'; +// import * as Egg from 'yadan'; -declare module 'egg' { - interface Application { - fromXiandan(): Promise; - } +// declare module 'egg' { +// interface Application { +// fromXiandan(): Promise; +// } - interface Context { - fromXiandan(): Promise; - } +// interface Context { +// fromXiandan(): Promise; +// } - interface EggAppConfig { - XiandanType: string; - } -} +// interface EggAppConfig { +// XiandanType: string; +// } +// } -declare module 'xiandan' { - class XiandanApplication extends Application { - superXiandan(): Promise; - } -} +// declare module 'xiandan' { +// class XiandanApplication extends Application { +// superXiandan(): Promise; +// } +// } -export = Egg; \ No newline at end of file +// export = Egg; diff --git a/test/fixtures/apps/app-ts-type-check/yadan.d.ts b/test/fixtures/apps/app-ts-type-check/yadan.d.ts index 16693e7600..98a0cc775a 100644 --- a/test/fixtures/apps/app-ts-type-check/yadan.d.ts +++ b/test/fixtures/apps/app-ts-type-check/yadan.d.ts @@ -1,23 +1,23 @@ -import * as Egg from 'egg'; +// import * as Egg from 'egg'; -declare module 'egg' { - interface Application { - fromYadan(): Promise; - } +// declare module 'egg' { +// interface Application { +// fromYadan(): Promise; +// } - interface Context { - fromYadan(): Promise; - } +// interface Context { +// fromYadan(): Promise; +// } - interface EggAppConfig { - yadanType: string; - } -} +// interface EggAppConfig { +// yadanType: string; +// } +// } -declare module 'yadan' { - class YadanApplication extends Application { - superYadan(): Promise; - } -} +// declare module 'yadan' { +// class YadanApplication extends Application { +// superYadan(): Promise; +// } +// } -export = Egg; +// export = Egg; diff --git a/test/fixtures/apps/demo/app.js b/test/fixtures/apps/demo/app.js index e9849e8d64..2f4c36fb4c 100644 --- a/test/fixtures/apps/demo/app.js +++ b/test/fixtures/apps/demo/app.js @@ -1,4 +1,4 @@ -const mm = require('egg-mock'); +const { mm } = require('@eggjs/mock'); class DemoAppTest { diff --git a/test/fixtures/apps/response/app/router.js b/test/fixtures/apps/response/app/router.js index 14e32a32c3..c55fada2f6 100644 --- a/test/fixtures/apps/response/app/router.js +++ b/test/fixtures/apps/response/app/router.js @@ -1,5 +1,3 @@ -'use strict'; - const assert = require('assert'); module.exports = app => { @@ -14,4 +12,8 @@ module.exports = app => { this.type = 'text'; this.response.type = 'plain/text'; }); + + app.get('/empty-json', async function () { + this.body = {}; + }); } diff --git a/test/lib/agent.test.js b/test/lib/agent.test.js index 791bce8def..e7f9800007 100644 --- a/test/lib/agent.test.js +++ b/test/lib/agent.test.js @@ -1,10 +1,8 @@ -'use strict'; - const assert = require('assert'); const fs = require('fs'); const path = require('path'); const execSync = require('child_process').execSync; -const mm = require('egg-mock'); +const mm = require('@eggjs/mock'); const utils = require('../utils'); describe('test/lib/agent.test.js', () => { diff --git a/test/utils.ts b/test/utils.ts index 009547b52d..a71abd2129 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,13 +1,20 @@ import { readFileSync } from 'node:fs'; +import { once } from 'node:events'; import { rm } from 'node:fs/promises'; import path from 'node:path'; import http from 'node:http'; import { fileURLToPath } from 'node:url'; import { AddressInfo } from 'node:net'; -import { mm, MockOption, MockApplication } from 'egg-mock'; +import { + mm, MockOptions, MockApplication, + // MockApplication as _MockApplication, +} from '@eggjs/mock'; import { Application as Koa } from '@eggjs/koa'; import request from 'supertest'; -import { startEgg, StartEggOptions, Application } from '../src/index.js'; +import { + startEgg, StartEggOptions, Application, + // ContextDelegation, +} from '../src/index.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const fixtures = path.join(__dirname, 'fixtures'); @@ -17,13 +24,18 @@ export async function rimraf(target: string) { await rm(target, { force: true, recursive: true }); } -export { MockOption, MockApplication, mm } from 'egg-mock'; +// export type MockApplication = Omit<_MockApplication, 'mockContext'> & { +// mockContext(data?: any, options?: any): ContextDelegation +// }; + +export { MockApplication, MockOptions, mm } from '@eggjs/mock'; export const restore = () => mm.restore(); -export function app(name: string | MockOption, options?: MockOption) { +export function app(name: string | MockOptions, options?: MockOptions) { options = formatOptions(name, options); const app = mm.app(options); return app; + // return app as unknown as MockApplication; } export const createApp = app; @@ -35,9 +47,9 @@ export const createApp = app; * @param {Object} [options] - optional * @return {App} app - Application object. */ -export function cluster(name: string | MockOption, options?: MockOption): MockApplication { +export function cluster(name: string | MockOptions, options?: MockOptions): MockApplication { options = formatOptions(name, options); - return mm.cluster(options); + return mm.cluster(options) as unknown as MockApplication; } export interface MockSingleProcessApplication extends Application { @@ -72,53 +84,51 @@ process.once('exit', () => { localServer = undefined; }); -export function startLocalServer() { - return new Promise(resolve => { - if (localServer) { - const address = localServer.address() as AddressInfo; - return resolve(`http://127.0.0.1:${address.port}`); +export async function startLocalServer() { + if (localServer) { + const address = localServer.address() as AddressInfo; + return `http://127.0.0.1:${address.port}`; + } + + let retry = false; + const app = new Koa(); + app.use(async ctx => { + if (ctx.path === '/get_headers') { + ctx.body = ctx.request.headers; + return; } - let retry = false; - const app = new Koa(); - app.use(async ctx => { - if (ctx.path === '/get_headers') { - ctx.body = ctx.request.headers; - return; - } + if (ctx.path === '/timeout') { + await exports.sleep(10000); + ctx.body = `${ctx.method} ${ctx.path}`; + return; + } - if (ctx.path === '/timeout') { - await exports.sleep(10000); - ctx.body = `${ctx.method} ${ctx.path}`; - return; - } + if (ctx.path === '/error') { + ctx.status = 500; + ctx.body = 'this is an error'; + return; + } - if (ctx.path === '/error') { + if (ctx.path === '/retry') { + if (!retry) { + retry = true; ctx.status = 500; - ctx.body = 'this is an error'; - return; - } - - if (ctx.path === '/retry') { - if (!retry) { - retry = true; - ctx.status = 500; - } else { - ctx.set('x-retry', '1'); - ctx.body = 'retry suc'; - retry = false; - } - return; + } else { + ctx.set('x-retry', '1'); + ctx.body = 'retry suc'; + retry = false; } + return; + } - ctx.body = `${ctx.method} ${ctx.path}`; - }); - localServer = http.createServer(app.callback()); - localServer.listen(0, () => { - const address = localServer!.address() as AddressInfo; - return resolve(`http://127.0.0.1:${address.port}`); - }); + ctx.body = `${ctx.method} ${ctx.path}`; }); + localServer = http.createServer(app.callback()); + localServer.listen(0); + await once(localServer, 'listening'); + const address = localServer!.address() as AddressInfo; + return `http://127.0.0.1:${address.port}`; } export function getFilepath(name: string) { @@ -129,7 +139,7 @@ export function getJSON(name: string) { return JSON.parse(readFileSync(getFilepath(name), 'utf-8')); } -function formatOptions(name: string | MockOption, options?: MockOption) { +function formatOptions(name: string | MockOptions, options?: MockOptions) { let baseDir; if (typeof name === 'string') { baseDir = name;