diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000000..8b1bbaf5ca --- /dev/null +++ b/index.d.ts @@ -0,0 +1,780 @@ +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, ...args: string[]): void; +} + +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 }; +} + +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; +} + +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; + + /** + * 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/package.json b/package.json index 8b762985d6..7ff166c8d6 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,9 @@ "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", "cluster-client": "^1.6.4", @@ -68,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/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..f9da879ccc --- /dev/null +++ b/test/fixtures/apps/app-ts/app/controller/foo.ts @@ -0,0 +1,18 @@ +import { Controller } from 'egg'; + +// add user controller and service +declare module 'egg' { + interface IController { + foo: FooController; + } +} + +// controller +export default class FooController extends Controller { + async getData() { + this.ctx.body = await this.ctx.service.foo.bar(); + } + async getBar() { + this.ctx.body = await this.service.foo.bar(); + } +} 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/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); + }); + }); + +}); +