diff --git a/.autod.conf.js b/.autod.conf.js index 9f9b068d36..167b1ec4f4 100644 --- a/.autod.conf.js +++ b/.autod.conf.js @@ -5,7 +5,6 @@ module.exports = { plugin: 'autod-egg', prefix: '^', devprefix: '^', - registry: 'https://r.cnpmjs.org', exclude: [ 'test/fixtures', 'examples', @@ -22,7 +21,10 @@ module.exports = { 'egg-plugin-puml', 'egg-view-nunjucks', ], - keep: [ + dep: [ + '@types/accepts', + '@types/koa', + '@types/koa-router', ], semver: [ 'eslint@3', diff --git a/.gitignore b/.gitignore index f6f655ce8a..bf11da8c85 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,6 @@ docs/**/contributing.md docs/public docs/plugins.png +package-lock.json +yarn.lock !test/fixtures/apps/loader-plugin/node_modules diff --git a/History.md b/History.md index b022d3e10e..da35ac186d 100644 --- a/History.md +++ b/History.md @@ -1,6 +1,41 @@ # History -## 2017-05-28, Version 1.3.1, @dead-horse +## 2017-06-21, Version 1.5.0, @fengmk2 + +* **feature** + * better TypeScript support, add `index.d.ts` file. + * enable overrideMethod middleware by default. +* **document** + * Documents improved. + +### Commits + + * [[`1d02601`](http://github.com/eggjs/egg/commit/1d026019df76525d2d9117c260eb5d892388121c)] - tsd: add another properties of FileStream (#1080) (Rwing <>) + * [[`2b1644e`](http://github.com/eggjs/egg/commit/2b1644e6d56e6481ee97bce009c5f53b4dd18441)] - feat: add tsd (#1027) (Eward Song <>) + * [[`a4ba2a2`](http://github.com/eggjs/egg/commit/a4ba2a2a1ef7de49e196c01447fd73ab22ed6d34)] - feat: enable overrideMethod middleware by default (#1069) (fengmk2 <>) + * [[`bfb8df5`](http://github.com/eggjs/egg/commit/bfb8df58bcc7d7fe0fd6ff3453efcb54b715b4a0)] - docs: typo (#1060) (chenbin92 <>) + * [[`64d1b00`](http://github.com/eggjs/egg/commit/64d1b0026648e1128f09efb6e6c2cc7f632bf608)] - docs: add chrome devtools debug information (#1050) (仙森 <>) + * [[`4e510b2`](http://github.com/eggjs/egg/commit/4e510b22836096a47d562dbd5ca8affd28f94f9e)] - chore: use app.httpRequest() instead of supertest (#1041) (fengmk2 <>) + * [[`78a13d5`](http://github.com/eggjs/egg/commit/78a13d52c3b6e8b40a0015b285cda33a059c0ee4)] - docs: add more description at quickstart (#1042) (TZ | 天猪 <>) + * [[`ef7c864`](http://github.com/eggjs/egg/commit/ef7c864fbddf7e70afbd93a16d5176787328400d)] - docs: add ant.design link (#1037) (Haoliang Gao <>) + * [[`f1b510c`](http://github.com/eggjs/egg/commit/f1b510c34039259c5772021432ab71a7a62b89e8)] - feat: add config.logger.disableConsoleAfterReady (#1001) (fengmk2 <>) + * [[`4890eda`](http://github.com/eggjs/egg/commit/4890eda31b9bc60ea4a1a7f36460ec1bf86dc134)] - docs: Uniform the standards that we should acquire this parsed parame… (#1038) (Ruanyq <>) + * [[`9d705e4`](http://github.com/eggjs/egg/commit/9d705e4687cdb98d327fbd9a1061604828218dfc)] - test: make sure app close (#1030) (fengmk2 <>) + * [[`1d72e37`](http://github.com/eggjs/egg/commit/1d72e3799822e252934d6218a978c2bd21f378d3)] - docs: fix caseStyle link (#1033) (Desen Meng <>) + * [[`9b50725`](http://github.com/eggjs/egg/commit/9b507250725ef3beda0ee51ac0c2bc2b007b2ecb)] - docs: (tutorials/index.md & async-function.md ): [translate] Done (#1028) (Darren Wong <>) + * [[`3d04199`](http://github.com/eggjs/egg/commit/3d041992912d9aca1eb0edc6b226474022e08236)] - docs: typo (#1029) (Jerry Wu <>) + * [[`13b7c19`](http://github.com/eggjs/egg/commit/13b7c19531d772a2b932ada28e186a0dbd0cf5f5)] - test: node 8 (#976) (fengmk2 <>) + * [[`1b108a7`](http://github.com/eggjs/egg/commit/1b108a72a96d3d8241b332b8e728a9ec409efbb1)] - docs: remove api that is from egg-rest (#1022) (Haoliang Gao <>) + * [[`057bc47`](http://github.com/eggjs/egg/commit/057bc47e4c5e3ec8faae0de3807f656fa4ef41d4)] - test: add doc test (#989) (Haoliang Gao <>) + * [[`c6eb7b2`](http://github.com/eggjs/egg/commit/c6eb7b2f59f24fe0c6a787829d33cdf0cd4a2e77)] - doc: fix view config doc (#991) (当轩 <>) + * [[`52865b4`](http://github.com/eggjs/egg/commit/52865b47c4d336833ef1151bae9f30867359ceb6)] - docs: devtool inspect at 8.x (#1018) (TZ | 天猪 <>) + * [[`8a120fd`](http://github.com/eggjs/egg/commit/8a120fde73df60e23f8c5559a3281acaf0a393e0)] - docs: remove max time limit at schdule (#995) (TZ | 天猪 <>) + * [[`9084c24`](http://github.com/eggjs/egg/commit/9084c24dd10fcbcd0d436ada9639b59f36dd2edf)] - docs: add plugin list (#988) (Haoliang Gao <>) + * [[`20a5d91`](http://github.com/eggjs/egg/commit/20a5d9127f7454c899f7701f02b04eefa7c61fce)] - test: disable coverage for schedule (#987) (Haoliang Gao <>) + * [[`3de963f`](http://github.com/eggjs/egg/commit/3de963f3881ef6fb9c5b6fa207730c6695853239)] - docs(basics/structure.md): [translate] (#970) (Weilun Xiong <<330815461@qq.com>>) + * [[`2f232f3`](http://github.com/eggjs/egg/commit/2f232f30b0ba7e14ab07c43e34d363bac3906a43)] - docs: file must appear after other fiels when using getFileStream (#982) (Yiyu He <>) + +## 2017-05-28, Version 1.4.0, @dead-horse * **feature** * use lru to aovid oom when httpclient dns cache enabled @@ -8,7 +43,6 @@ * fix port is missed when httpclient dns cache enabled * fix request url object will be changed when httpclient dns cache enabled * set maxSockets defautl value to Number.MAX_SAFE_INTEGER - * **document** * Documents improved. Thanks @DarrenWong, @zousandian, @lslxdx, @Azard, @johnnychen, @coogleyao, @DanielWLam, @m31271n, @Brian175 diff --git a/app/middleware/override_method.js b/app/middleware/override_method.js new file mode 100644 index 0000000000..bf5f7ef0f4 --- /dev/null +++ b/app/middleware/override_method.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('koa-override'); diff --git a/config/config.default.js b/config/config.default.js index 9a402c3688..de9c96dc43 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -155,7 +155,7 @@ module.exports = appInfo => { * * @member {Object} Config#siteFile - key is path, and value is url or buffer. * @example - * // 指定应用 favicon, => '/favicon.ico': 'https://eggjs.org/favicon.ico', + * // specific app's favicon, => '/favicon.ico': 'https://eggjs.org/favicon.ico', * config.siteFile = { * '/favicon.ico': 'https://eggjs.org/favicon.ico', * }; @@ -168,16 +168,16 @@ module.exports = appInfo => { * The option of `bodyParser` middleware * * @member Config#bodyParser - * @property {Boolean} enable - enable bodyParser or not, default to true + * @property {Boolean} enable - enable bodyParser or not, default is 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 的编码格式,默认为 utf8 - * @property {String} formLimit - form body 的大小限制,默认为 100kb - * @property {String} jsonLimit - json body 的大小限制,默认为 100kb - * @property {Boolean} strict - json body 解析是否为严格模式,如果为严格模式则只接受 object 和 array - * @property {Number} queryString.arrayLimit - 表单元素数组长度限制,默认 100,否则会转换为 json 格式 - * @property {Number} queryString.depth - json 数值深度限制,默认 5 - * @property {Number} queryString.parameterLimit - 参数个数限制,默认 1000 + * @property {String} encoding - body's encoding type,default is utf8 + * @property {String} formLimit - limit of the urlencoded body. If the body ends up being larger than this limit, a 413 error code is returned. Default is 100kb + * @property {String} jsonLimit - limit of the json body, default is 100kb + * @property {Boolean} strict - when set to true, JSON parser will only accept arrays and objects. Default is true + * @property {Number} queryString.arrayLimit - urlencoded body array's max length, default is 100 + * @property {Number} queryString.depth - urlencoded body object's max depth, default is 5 + * @property {Number} queryString.parameterLimit - urlencoded body maximum parameters, default is 1000 */ config.bodyParser = { enable: true, @@ -254,6 +254,7 @@ module.exports = appInfo => { 'siteFile', 'notfound', 'bodyParser', + 'overrideMethod', ]; /** diff --git a/docs/source/_data/links.yml b/docs/source/_data/links.yml index 7911ddba75..ed3f5c19d2 100644 --- a/docs/source/_data/links.yml +++ b/docs/source/_data/links.yml @@ -7,3 +7,4 @@ Community: Issues: https://github.com/eggjs/egg/issues Links: Ant Design: https://ant.design + Enclose.IO: http://enclose.io diff --git a/docs/source/en/basics/config.md b/docs/source/en/basics/config.md index e817046846..56efdac49d 100644 --- a/docs/source/en/basics/config.md +++ b/docs/source/en/basics/config.md @@ -38,6 +38,16 @@ module.exports = { }, }; ``` + +The configuration file can simplify to `exports.key = value` format + +```js +exports.keys = 'my-cookie-secret-key'; +exports.logger = { + level: 'DEBUG', +}; +``` + The configuration file can also return a function which could receive a parameter called `appInfo` ```js @@ -75,7 +85,7 @@ Here is one sequence of loading configurations under "prod" environment, in whic -> plugin config.prod.js -> framework config.prod.js -> application config.prod.js - + **Note: there will be plugin loading sequence, but the approximate order is similar. For specific logic, please check the [loader](../advanced/loader.md) .** ### Merging rule diff --git a/docs/source/en/intro/index.md b/docs/source/en/intro/index.md index 372004ac69..ff71a099a5 100644 --- a/docs/source/en/intro/index.md +++ b/docs/source/en/intro/index.md @@ -18,12 +18,12 @@ Egg is a convention-over-configuration framework, follows the [Loader](../advanc ## features -- depth [customizd framework](../advanced/framework.md) -- highly extensible [plug-in mechanism](../advanced/plugin.md) -- built-in [cluster](../advanced/cluster-client.md) -- based on [koa] with high performance -- stable core framework with high test coverage. -- [progressive development](../tutorials/progressive.md) +- Provide capability to [customizd framework](../advanced/framework.md) base on Egg +- Highly extensible [plug-in mechanism](../advanced/plugin.md) +- Built-in [cluster](../advanced/cluster-client.md) +- Based on [Koa] with high performance +- Stable core framework with high test coverage +- [Progressive development](../tutorials/progressive.md) [Sails]: http://sailsjs.com [Express]: http://expressjs.com diff --git a/docs/source/en/intro/quickstart.md b/docs/source/en/intro/quickstart.md index 117bd81bf1..db3e8cfc1c 100644 --- a/docs/source/en/intro/quickstart.md +++ b/docs/source/en/intro/quickstart.md @@ -90,11 +90,10 @@ module.exports = app => { }; ``` -Then add a configuration file: +Then add a [configuration](../basics/config.md) file: ```js // config/config.default.js -// should change to your own keys exports.keys = ; ``` @@ -174,18 +173,13 @@ exports.nunjucks = { ```js // config/config.default.js -module.exports = appInfo => { - const config = {}; - config.keys = ; - - // add config - config.view = { - defaultViewEngine: 'nunjucks', - mapping: { - '.tpl': 'nunjucks', - }, - } - return config; +exports.keys = ; +// add view's configurations +exports.view = { + defaultViewEngine: 'nunjucks', + mapping: { + '.tpl': 'nunjucks', + }, }; ``` @@ -308,6 +302,7 @@ And also add config. ```js // config/config.default.js +// add news' configurations exports.news = { pageSize: 5, serverUrl: 'https://hacker-news.firebaseio.com/v0', @@ -367,11 +362,11 @@ module.exports = (options, app) => { }; // config/config.default.js -// mount middleware +// add middleware robot exports.middleware = [ 'robot' ]; -// middleware config +// robot's configurations exports.robot = { ua: [ /Baiduspider/i, diff --git a/docs/source/zh-cn/basics/config.md b/docs/source/zh-cn/basics/config.md index de6d7d08a9..863c35282c 100644 --- a/docs/source/zh-cn/basics/config.md +++ b/docs/source/zh-cn/basics/config.md @@ -41,6 +41,15 @@ module.exports = { }; ``` +配置文件也可以简化的写成 `exports.key = value` 形式 + +```js +exports.keys = 'my-cookie-secret-key'; +exports.logger = { + level: 'DEBUG', +}; +``` + 配置文件也可以返回一个 function,可以接受 appInfo 参数 ```js diff --git a/docs/source/zh-cn/core/cluster-and-ipc.md b/docs/source/zh-cn/core/cluster-and-ipc.md index deaa78f932..71e656a02e 100644 --- a/docs/source/zh-cn/core/cluster-and-ipc.md +++ b/docs/source/zh-cn/core/cluster-and-ipc.md @@ -284,14 +284,14 @@ if (cluster.isMaster) { - `app.messenger.broadcast(action, data)`:发送给所有的 agent / app 进程(包括自己) - `app.messenger.sendToApp(action, data)`: 发送给所有的 app 进程 - - 在 app 调用该方法上会发送给自己和其他的 app 进程 + - 在 app 上调用该方法会发送给自己和其他的 app 进程 - 在 agent 上调用该方法会发送给所有的 app 进程 - `app.messenger.sendToAgent(action, data)`: 发送给 agent 进程 - - 在 app 调用该方法上会发送 agent 进程 + - 在 app 上调用该方法会发送给 agent 进程 - 在 agent 上调用该方法会发送给 agent 自己 - `agent.messenger.sendRandom(action, data)`: - app 上没有该方法(现在 Egg 的实现是等同于 sentToAgent) - - agent 上回随机发送消息给一个 app 进程(由 master 来控制发送给谁) + - agent 会随机发送消息给一个 app 进程(由 master 来控制发送给谁) - `app.messenger.sendTo(pid, action, data)`: 发送给指定进程 ```js diff --git a/docs/source/zh-cn/intro/index.md b/docs/source/zh-cn/intro/index.md index 8ae96a706f..4d5c6674fc 100644 --- a/docs/source/zh-cn/intro/index.md +++ b/docs/source/zh-cn/intro/index.md @@ -21,7 +21,7 @@ Egg 奉行『**约定优于配置**』,按照[一套统一的约定](../advanc ## 特性 -- 深度[框架定制](../advanced/framework.md) +- 提供基于 Egg [定制上层框架](../advanced/framework.md)的能力 - 高度可扩展的[插件机制](../advanced/plugin.md) - 内置[多进程管理](../advanced/cluster-client.md) - 基于 [Koa] 开发,性能优异 diff --git a/docs/source/zh-cn/intro/quickstart.md b/docs/source/zh-cn/intro/quickstart.md index b890481589..8c6ea2b0ff 100644 --- a/docs/source/zh-cn/intro/quickstart.md +++ b/docs/source/zh-cn/intro/quickstart.md @@ -84,11 +84,10 @@ module.exports = app => { }; ``` -加一个配置文件: +加一个[配置文件](../basics/config.md): ```js // config/config.default.js -// 切记:要改为自己的 key 值 exports.keys = <此处改为你自己的 Cookie 安全字符串>; ``` @@ -162,18 +161,13 @@ exports.nunjucks = { ```js // config/config.default.js -module.exports = appInfo => { - const config = {}; - config.keys = <此处改为你自己的 Cookie 安全字符串>; - - // 添加配置 - config.view = { - defaultViewEngine: 'nunjucks', - mapping: { - '.tpl': 'nunjucks', - }, - } - return config; +exports.keys = <此处改为你自己的 Cookie 安全字符串>; +// 添加 view 配置 +exports.view = { + defaultViewEngine: 'nunjucks', + mapping: { + '.tpl': 'nunjucks', + }, }; ``` @@ -289,6 +283,7 @@ module.exports = app => { ```js // config/config.default.js +// 添加 news 的配置项 exports.news = { pageSize: 5, serverUrl: 'https://hacker-news.firebaseio.com/v0', @@ -341,11 +336,11 @@ module.exports = (options, app) => { }; // config/config.default.js -// mount middleware +// add middleware robot exports.middleware = [ 'robot' ]; -// middleware config +// robot's configurations exports.robot = { ua: [ /Baiduspider/i, diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000000..eba965d946 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,803 @@ +import * as accepts from 'accepts'; +import * as KoaApplication from 'koa'; +import * as KoaRouter from 'koa-router'; +import { Readable } from 'stream'; +/** + * BaseContextClass is a base class that can be extended, + * it's instantiated in context level, + * {@link Helper}, {@link Service} is extending it. + */ +declare class BaseContextClass { // tslint:disable-line + /** + * request context + */ + ctx: Context; + + /** + * Application + */ + app: Application; + + /** + * Application config object + */ + config: EggAppConfig; + + /** + * service + */ + service: IService; + + constructor(ctx: Context); +} + +export interface Logger { + info(info: string, ...args: string[]): void; + warn(info: string, ...args: string[]): void; + debug(info: string, ...args: string[]): void; + error(info: string | Error, ...args: string[]): void; +} + +export type RequestArrayBody = any[]; +export type RequestObjectBody = { [key: string]: any }; +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: { [key: string]: string[] }; + + /** + * 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: { [key: string]: string }; + + body: RequestArrayBody | RequestObjectBody; +} + +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; +} + +interface ContextView { // tslint:disable-line + /** + * Render a file by view engine + * @param {String} name - the file path based on root + * @param {Object} [locals] - data used by template + * @param {Object} [options] - view options, you can use `options.viewEngine` to specify view engine + * @return {Promise} result - return a promise with a render result + */ + render(name: string, locals: any, options?: any): Promise; + + /** + * Render a template string by view engine + * @param {String} tpl - template string + * @param {Object} [locals] - data used by template + * @param {Object} [options] - view options, you can use `options.viewEngine` to specify view engine + * @return {Promise} result - return a promise with a render result + */ + renderString(name: string, locals: any, options?: any): Promise; +} + +export type LoggerLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'NONE'; + +export interface EggAppConfig { + workerStartTimeout: number; + baseDir: 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 100kb + * @property {String} jsonLimit - json body size limit, default 100kb + * @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 lenght, default 5 + * @property {Number} queryString.parameterLimit - paramter number limit ,default 1000 + */ + bodyParser: { + enable: boolean; + encoding: string; + formLimit: string; + jsonLimit: string; + strict: true; + queryString: { + arrayLimit: number; + depth: number; + parameterLimit: number; + }; + }; + + /** + * 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} 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 + */ + logger: { + dir: string; + encoding: string; + env: string; + level: LoggerLevel; + consoleLevel: LoggerLevel; + outputJSON: boolean; + buffer: boolean; + appLogName: string; + coreLogName: string; + agentLogName: string; + errorLogName: string; + coreLogger: any; + }; + + httpclient: { + keepAlive: boolean; + freeSocketKeepAliveTimeout: number; + timeout: number; + maxSockets: number; + maxFreeSockets: number; + enableDNSCache: boolean; + }; + + 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; + }; + /** + * It will ignore special keys when dumpConfig + */ + dump: { + ignore: Set; + }; + + /** + * The environment of egg + */ + env: string; + + /** + * 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 + */ + dir: 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; + xframe: { + enable: boolean; + value: 'SAMEORIGIN' | 'DENY' | 'ALLOW-FROM'; + }; + hsts: any; + methodnoallow: { enable: boolean }; + noopen: { enable: boolean; } + xssProtection: any; + csp: any; + }; + + siteFile: any; + + static: any; + + view: any; + + watcher: any; +} + +export interface Router extends KoaRouter { + /** + * 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): any; +} + +declare interface EggApplication extends KoaApplication { // tslint:disable-line + /** + * The current directory of application + */ + baseDir: string; + + /** + * The configuration of application + */ + config: EggAppConfig; + + /** + * app.env delegate app.config.env + */ + env: string; + + /** + * core logger for framework and plugins, log file is $HOME/logs/{appname}/egg-web + */ + coreLogger: Logger; + + /** + * Alias to https://npmjs.com/package/depd + */ + deprecate: any; + + /** + * HttpClient instance + */ + httpclient: any; + + /** + * The loader instance, the default class is EggLoader. If you want define + */ + loader: 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: Logger; + + /** + * All loggers contain logger, coreLogger and customLogger + */ + loggers: { [loggerName: string]: Logger }; + + /** + * messenger instance + */ + messenger: any; + + plugins: any; + + /** + * get router + */ + router: Router; + + Service: Service; + + /** + * Whether `application` or `agent` + */ + type: string; + + /** + * create a singleton instance + */ + addSingleton(name: string, create: any): void; + + /** + * Excute scope after loaded and before app start + */ + beforeStart(scrope: () => void): void; + + /** + * Close all, it wil close + * - callbacks registered by beforeClose + * - emit `close` event + * - remove add listeners + * + * If error is thrown when it's closing, the promise will reject. + * It will also reject after following call. + * @return {Promise} promise + * @since 1.0.0 + */ + close(): 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(url: string, opt: any): Promise; + + /** + * Get logger by name, it's equal to app.loggers['name'], but you can extend it with your own logical + */ + getLogger(name: string): Logger; + + /** + * print the infomation when console.log(app) + */ + inspect(): any; + + /** + * Alias to Router#url + */ + url(name: string, params: any): any; +} + +export interface Application extends EggApplication { + /** + * global locals for view + * @see Context#locals + */ + locals: any; + + /** + * HTTP get method + */ + get(path: string, fn: string): void; + get(path: string, ...middleware: any[]): void; + + /** + * HTTP post method + */ + post(path: string, fn: string): void; + post(path: string, ...middleware: any[]): void; + + /** + * HTTP put method + */ + put(path: string, fn: string): void; + put(path: string, ...middleware: any[]): void; + + /** + * HTTP delete method + */ + delete(path: string, fn: string): void; + delete(path: string, ...middleware: any[]): void; + + /** + * restful router api + */ + resources(name: string, prefix: string, fn: string): Router; + + redirect(path: string, redirectPath: string): void; + + controller: IController; + + Controller: Controller; +} + +interface FileStream extends Readable { // tslint:disable-line + fields: any; + + filename: string; + + fieldname: string; + + mime: string; + + mimeType: string; + + transferEncoding: string; + + encoding: string; + + truncated: boolean; +} + +export interface Context extends KoaApplication.Context { + app: Application; + + service: IService; + + request: Request; + + response: Response; + + /** + * 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#accept + */ + queries: { [key: string]: string[] }; + + /** + * @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: any; + + /** + * 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: Logger; + + /** + * Request start time + */ + starttime: number; + + /** + * View instance that is created every request + */ + view: ContextView; + + /** + * 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(url: string, opt: any): Promise; + + /** + * Get logger by name, it's equal to app.loggers['name'], but you can extend it with your own logical + */ + getLogger(name: string): Logger; + + /** + * Render a file by view engine + * @param {String} name - the file path based on root + * @param {Object} [locals] - data used by template + * @param {Object} [options] - view options, you can use `options.viewEngine` to specify view engine + * @return {Promise} result - return a promise with a render result + */ + render(name: string, locals: any, options?: any): Promise; + + /** + * Render a template string by view engine + * @param {String} tpl - template string + * @param {Object} [locals] - data used by template + * @param {Object} [options] - view options, you can use `options.viewEngine` to specify view engine + * @return {Promise} result - return a promise with a render result + */ + renderString(name: string, locals: any, options?: any): Promise; + + __(key: string, ...values: string[]): string; + gettext(key: string, ...values: string[]): string; + + /** + * get upload file stream + * @example + * ```js + * const stream = yield this.getFileStream(); + * // get other fields + * console.log(stream.fields); + * ``` + * @method Context#getFileStream + * @return {ReadStream} stream + * @since 1.0.0 + */ + getFileStream(): Promise; + + /** + * @see Responce.redirect + */ + redirect(url: string, alt?: string): void; +} + +export class Controller extends BaseContextClass { } + +export class Service extends BaseContextClass { } + +/** + * The empty interface `IService` is an placehoder, 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 { }// tslint:disable-line + +export interface IController { } // tslint:disable-line + diff --git a/lib/application.js b/lib/application.js index 36609eba59..22318e25ba 100644 --- a/lib/application.js +++ b/lib/application.js @@ -120,7 +120,7 @@ class Application extends EggApplication { } /** - * Create an anonymouse context, the context isn't request level, so the request is mocked. + * 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} Application#createAnonymousContext * @param {Request} req - if you want to mock request like querystring, you can pass an object to this function. diff --git a/package.json b/package.json index 3cb91bbb68..8e9ca0aa3f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "egg", - "version": "1.4.0", + "version": "1.5.0", "description": "A web framework's framework for Node.js", "keywords": [ "web", @@ -13,24 +13,27 @@ "egg" ], "dependencies": { + "@types/accepts": "^1.3.2", + "@types/koa": "^2.0.39", + "@types/koa-router": "^7.0.22", "accepts": "^1.3.3", - "agentkeepalive": "^3.2.0", + "agentkeepalive": "^3.3.0", "cluster-client": "^1.6.4", "co": "^4.6.0", "debug": "^2.6.8", "delegates": "^1.0.0", - "egg-cluster": "^1.7.0", + "egg-cluster": "^1.8.0", "egg-cookies": "^2.2.1", - "egg-core": "^3.10.0", + "egg-core": "^3.11.0", "egg-development": "^1.3.1", "egg-i18n": "^1.1.1", "egg-jsonp": "^1.1.1", "egg-logger": "^1.6.0", "egg-logrotator": "^2.2.3", "egg-multipart": "^1.5.0", - "egg-onerror": "^1.4.4", + "egg-onerror": "^1.4.6", "egg-schedule": "^2.4.1", - "egg-security": "^1.10.1", + "egg-security": "^1.11.0", "egg-session": "^2.1.1", "egg-static": "^1.4.1", "egg-view": "^1.1.1", @@ -40,6 +43,7 @@ "is-type-of": "^1.0.0", "koa-bodyparser": "^2.5.0", "koa-is-json": "^1.0.0", + "koa-override": "^2.0.0", "mime-types": "^2.1.15", "sendmessage": "^1.1.0", "urllib": "^2.22.0", @@ -49,15 +53,15 @@ "devDependencies": { "autod": "^2.8.0", "autod-egg": "^1.0.0", - "coffee": "^3.3.2", + "coffee": "^4.0.1", "egg-alinode": "^1.0.3", - "egg-bin": "^3.4.2", + "egg-bin": "^4.0.4", "egg-doctools": "^2.0.0", - "egg-mock": "^3.7.2", + "egg-mock": "^3.8.0", "egg-plugin-puml": "^2.4.0", "egg-view-nunjucks": "^2.1.2", "eslint": "^3.19.0", - "eslint-config-egg": "^4.2.0", + "eslint-config-egg": "^4.2.1", "findlinks": "^1.1.0", "formstream": "^1.1.0", "glob": "^7.1.2", @@ -67,15 +71,19 @@ "runscript": "^1.2.1", "spy": "^1.0.0", "supertest": "^3.0.0", + "ts-node": "^3.0.6", + "typescript": "^2.3.4", "webstorm-disable-index": "^1.1.2" }, "main": "index.js", + "types": "index.d.ts", "files": [ "app", "config", "bin", "lib", - "index.js" + "index.js", + "index.d.ts" ], "scripts": { "lint": "eslint app config lib test *.js", diff --git a/test/app/middleware/override_method.test.js b/test/app/middleware/override_method.test.js new file mode 100644 index 0000000000..80622e43ff --- /dev/null +++ b/test/app/middleware/override_method.test.js @@ -0,0 +1,55 @@ +'use strict'; + +const utils = require('../../utils'); + +describe('test/app/middleware/override_method.test.js', () => { + let app; + before(() => { + app = utils.app('apps/override_method'); + return app.ready(); + }); + after(() => app.close()); + + it('should put', () => { + app.mockCsrf(); + return app.httpRequest() + .post('/test') + .send({ _method: 'PUT' }) + .expect(200) + .expect('test-put'); + }); + + it('should patch', () => { + app.mockCsrf(); + return app.httpRequest() + .post('/test') + .send({ _method: 'PATCH' }) + .expect(200) + .expect('test-patch'); + }); + + it('should delete', () => { + return app.httpRequest() + .post('/test') + .send({ _method: 'DELETE' }) + .expect(200) + .expect('test-delete'); + }); + + it('should not work on PUT request', () => { + app.mockCsrf(); + return app.httpRequest() + .put('/test') + .send({ _method: 'DELETE' }) + .expect(200) + .expect('test-put'); + }); + + it('should not work on GET request', () => { + return app.httpRequest() + .get('/test') + .set('x-http-method-override', 'DELETE') + .expect(200) + .expect('test-get'); + }); +}); diff --git a/test/fixtures/apps/app-ts/.gitignore b/test/fixtures/apps/app-ts/.gitignore new file mode 100644 index 0000000000..9c0069648f --- /dev/null +++ b/test/fixtures/apps/app-ts/.gitignore @@ -0,0 +1,2 @@ +*.js +node_modules \ No newline at end of file diff --git a/test/fixtures/apps/app-ts/app/controller/foo.ts b/test/fixtures/apps/app-ts/app/controller/foo.ts new file mode 100644 index 0000000000..ba5aefbd42 --- /dev/null +++ b/test/fixtures/apps/app-ts/app/controller/foo.ts @@ -0,0 +1,27 @@ +import { Controller, RequestObjectBody } from 'egg'; + +// add user controller and service +declare module 'egg' { + interface IController { + foo: FooController; + } +} + +// controller +export default class FooController extends Controller { + async getData() { + try { + this.ctx.body = await this.ctx.service.foo.bar(); + } catch (e) { + const body: RequestObjectBody = this.ctx.request.body; + this.app.logger.info(e.name, body.foo); + } + } + async getBar() { + try { + this.ctx.body = await this.service.foo.bar(); + } catch (e) { + this.ctx.logger.error(e); + } + } +} diff --git a/test/fixtures/apps/app-ts/app/router.ts b/test/fixtures/apps/app-ts/app/router.ts new file mode 100644 index 0000000000..c5f82c80b3 --- /dev/null +++ b/test/fixtures/apps/app-ts/app/router.ts @@ -0,0 +1,7 @@ +import { Application } from 'egg'; + +export default (app: Application) => { + const controller = app.controller; + app.get('/foo', controller.foo.getData); + app.post('/', controller.foo.getData); +} diff --git a/test/fixtures/apps/app-ts/app/service/foo.ts b/test/fixtures/apps/app-ts/app/service/foo.ts new file mode 100644 index 0000000000..5f383495fc --- /dev/null +++ b/test/fixtures/apps/app-ts/app/service/foo.ts @@ -0,0 +1,14 @@ +import { Service } from 'egg'; + +// add user controller and service +declare module 'egg' { + interface IService { + foo: FooService; + } +} + +export default class FooService extends Service { + async bar() { + return { env: this.config.env }; + } +} diff --git a/test/fixtures/apps/app-ts/config/config.ts b/test/fixtures/apps/app-ts/config/config.ts new file mode 100644 index 0000000000..6c62f5ce03 --- /dev/null +++ b/test/fixtures/apps/app-ts/config/config.ts @@ -0,0 +1,3 @@ +export default { + keys: 'foo', +} diff --git a/test/fixtures/apps/app-ts/package.json b/test/fixtures/apps/app-ts/package.json new file mode 100644 index 0000000000..292c04d533 --- /dev/null +++ b/test/fixtures/apps/app-ts/package.json @@ -0,0 +1,4 @@ +{ + "name": "app-ts", + "version": "1.0.0" +} \ No newline at end of file diff --git a/test/fixtures/apps/app-ts/tsconfig.json b/test/fixtures/apps/app-ts/tsconfig.json new file mode 100644 index 0000000000..a58c3ffcb2 --- /dev/null +++ b/test/fixtures/apps/app-ts/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es6", + "baseUrl": ".", + "paths": { + "egg": ["../../../../index"] + }, + "module": "commonjs", + "lib": ["es7"], + "strict": true + } +} diff --git a/test/fixtures/apps/override_method/app/router.js b/test/fixtures/apps/override_method/app/router.js new file mode 100644 index 0000000000..45e07cf65f --- /dev/null +++ b/test/fixtures/apps/override_method/app/router.js @@ -0,0 +1,17 @@ +module.exports = app => { + app.get('/test', function* () { + this.body = "test-get"; + }); + + app.put('/test', function* () { + this.body = "test-put"; + }); + + app.delete('/test', function* () { + this.body = 'test-delete'; + }); + + app.patch('/test', function* () { + this.body = 'test-patch'; + }); +}; diff --git a/test/fixtures/apps/override_method/config/config.default.js b/test/fixtures/apps/override_method/config/config.default.js new file mode 100644 index 0000000000..b6cba897c8 --- /dev/null +++ b/test/fixtures/apps/override_method/config/config.default.js @@ -0,0 +1 @@ +exports.keys = 'foo'; diff --git a/test/fixtures/apps/override_method/package.json b/test/fixtures/apps/override_method/package.json new file mode 100644 index 0000000000..73b8ebb113 --- /dev/null +++ b/test/fixtures/apps/override_method/package.json @@ -0,0 +1,3 @@ +{ + "name": "override_method" +} diff --git a/test/lib/application.test.js b/test/lib/application.test.js index 8a1c454683..917742f545 100644 --- a/test/lib/application.test.js +++ b/test/lib/application.test.js @@ -137,6 +137,7 @@ describe('test/lib/application.test.js', () => { it('should warn if confused configurations exist', function* () { const app = utils.app('apps/confused-configuration'); yield app.ready(); + yield sleep(1000); const logs = fs.readFileSync(utils.getFilepath('apps/confused-configuration/logs/confused-configuration/confused-configuration-web.log'), 'utf8'); assert(logs.match(/Unexpected config key `bodyparser` exists, Please use `bodyParser` instead\./)); assert(logs.match(/Unexpected config key `notFound` exists, Please use `notfound` instead\./)); diff --git a/test/lib/core/loader/config_loader.test.js b/test/lib/core/loader/config_loader.test.js index e0967c08c4..40a9978930 100644 --- a/test/lib/core/loader/config_loader.test.js +++ b/test/lib/core/loader/config_loader.test.js @@ -14,12 +14,13 @@ describe('test/lib/core/loader/config_loader.test.js', () => { it('should get middlewares', function* () { app = utils.app('apps/demo'); yield app.ready(); - assert.deepEqual(app.config.coreMiddleware.slice(0, 6), [ + assert.deepEqual(app.config.coreMiddleware.slice(0, 7), [ 'meta', 'siteFile', 'notfound', 'static', 'bodyParser', + 'overrideMethod', 'session', ]); }); diff --git a/test/ts/index.test.js b/test/ts/index.test.js new file mode 100644 index 0000000000..05d823f848 --- /dev/null +++ b/test/ts/index.test.js @@ -0,0 +1,41 @@ +'use strict'; + +const request = require('supertest'); +const mm = require('egg-mock'); +const runscript = require('runscript'); +const path = require('path'); +const utils = require('../utils'); +const baseDir = path.join(__dirname, '../fixtures/apps/app-ts'); + +describe('test/ts/index.test.js', () => { + before(function* () { + if (process.env.CI) { + yield runscript('tsc && npmlink ../../../../', { cwd: baseDir }); + } else { + yield runscript('tsc && npm link ../../../../', { cwd: baseDir }); + } + }); + + describe('compiler code', () => { + + afterEach(mm.restore); + let app; + before(function* () { + app = utils.app('apps/app-ts'); + yield app.ready(); + }); + after(function* () { + yield app.close(); + }); + + it('controller run ok', done => { + request(app.callback()) + .get('/foo') + .expect(200) + .expect({ env: 'unittest' }) + .end(done); + }); + }); + +}); +