├── . gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── __test__ ├── app-test-example │ ├── controllers │ │ └── home.controller.ts │ ├── static │ │ └── test.text │ └── views │ │ └── index.html ├── config-all-test-example │ └── configs │ │ ├── default.config.yaml │ │ └── test.config.yaml ├── config-default-test-example │ └── configs │ │ └── default.config.yaml ├── controller-test-example │ ├── controllers │ │ └── home.controller.ts │ └── middlewares │ │ └── logger.middleware.ts ├── controller-with-invalid-middleware-test-example │ ├── controllers │ │ └── home.controller.ts │ └── middlewares │ │ └── limit.middleware.ts ├── invalid-controller-test-example │ ├── controllers │ │ └── home.controller.ts │ └── middlewares │ │ └── logger.middleware.ts ├── invalid-middleware-test-example │ ├── controllers │ │ └── home.controller.ts │ └── middlewares │ │ └── limit.middleware.ts ├── invalid-service-test-example │ └── services │ │ └── invalid.service.ts ├── middleware-test-example │ ├── controllers │ │ └── home.controller.ts │ └── middlewares │ │ └── logger.middleware.ts └── service-test-example │ └── services │ ├── log.service.ts │ └── user.service.ts ├── contributing.md ├── doc └── useage.md ├── example ├── advanced │ ├── .gitignore │ ├── app.ts │ ├── configs │ │ ├── default.config.yaml │ │ ├── development.config.yaml │ │ ├── production.config.yaml │ │ └── test.config.yaml │ ├── controllers │ │ ├── todo.controller.ts │ │ └── user.controller.ts │ ├── middlewares │ │ └── logger.middleware.ts │ ├── services │ │ ├── orm.service.ts │ │ └── user.service.ts │ ├── static │ │ └── test.text │ ├── tsconfig.json │ └── views │ │ └── index.html └── basic │ ├── app.ts │ ├── controllers │ └── home.controller.ts │ └── tsconfig.json ├── index.test.ts ├── index.ts ├── kost.png ├── package.json ├── scripts └── test.js ├── src ├── app.test.ts ├── app.ts ├── class │ ├── context.test.ts │ ├── context.ts │ ├── controller.test.ts │ ├── controller.ts │ ├── middleware.test.ts │ ├── middleware.ts │ ├── service.test.ts │ └── service.ts ├── config.test.ts ├── config.ts ├── const.test.ts ├── const.ts ├── decorators │ ├── http.test.ts │ ├── http.ts │ ├── index.ts │ ├── middleware.test.ts │ └── middleware.ts ├── path.test.ts ├── path.ts ├── utils.test.ts └── utils.ts ├── tsconfig.json └── yarn.lock /. gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # JS and TS files must always use LF for tools to work 5 | *.js eol=lf 6 | bin/* eol=lf 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: https://github.com/axetroy/buy-me-a-cup-of-tea 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **I'm submitting a ...** (check one with "x") 2 | 3 | - [ ] bug report => search github for a similar issue or PR before submitting 4 | - [ ] feature request 5 | - [ ] support request 6 | 7 | **Current behavior** 8 | 9 | 10 | **Expected behavior** 11 | 12 | 13 | **More detail** 14 | 15 | 16 | **Please tell us about your environment:** 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Please check if the PR fulfills these requirements** 2 | - [ ] The commit message follows **commit message standard** 3 | - [ ] Tests for the changes have been added (for bug fixes / features) 4 | - [ ] Docs have been added / updated (for bug fixes / features) 5 | 6 | 7 | **What kind of change does this PR introduce?** (check one with "x") 8 | ``` 9 | [ ] Bugfix 10 | [ ] Feature 11 | [ ] Code style update (formatting, local variables) 12 | [ ] Refactoring (no functional changes, no api changes) 13 | [ ] Test change 14 | [ ] Chore change 15 | [ ] Update Document 16 | [ ] Other... Please describe: 17 | ``` 18 | 19 | **What is the current behavior?** (You can also link to an open issue here) 20 | 21 | 22 | 23 | **What is the new behavior?** 24 | 25 | 26 | 27 | **Does this PR introduce a breaking change?** (check one with "x") 28 | ``` 29 | [ ] Yes 30 | [ ] No 31 | ``` 32 | 33 | If this PR contains a breaking change, please describe the impact and migration path for existing applications: ... 34 | 35 | 36 | **Other information**: 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .vscode 4 | build 5 | .nyc_output 6 | .idea -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # node-waf configuration 27 | .lock-wscript 28 | 29 | # Compiled binary addons (http://nodejs.org/api/addons.html) 30 | build/Release 31 | 32 | # Dependency directories 33 | node_modules 34 | jspm_packages 35 | 36 | # Optional npm cache directory 37 | .npm 38 | 39 | # Optional eslint cache 40 | .eslintcache 41 | 42 | # Optional REPL history 43 | .node_repl_history 44 | 45 | # Output of 'npm pack' 46 | *.tgz 47 | 48 | # Yarn Integrity file 49 | .yarn-integrity 50 | 51 | # idea 52 | .idea 53 | 54 | # test 55 | test 56 | 57 | # github 58 | .github 59 | 60 | # vscode 61 | .vscode 62 | 63 | # docs 64 | docs 65 | 66 | # others 67 | src 68 | .all-contributorsrc 69 | .editorconfig 70 | .gitattributes 71 | .gitignore 72 | .pullapprove.yml 73 | .travis.yml 74 | contributing.md 75 | *.gif 76 | *.map 77 | *.test.js 78 | .nyc_output 79 | build/__test__ 80 | build/src 81 | build/example 82 | example 83 | __test__ 84 | .idea 85 | .github -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | 4 | os: 5 | - linux 6 | 7 | node_js: 8 | - 8.9.0 9 | 10 | cache: 11 | yarn: true 12 | directories: 13 | - node_modules 14 | 15 | script: 16 | - npm test 17 | 18 | after_script: 19 | - nyc report --reporter=text-lcov | ./node_modules/.bin/coveralls 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 axetroy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Kost 2 | 3 | [![Build Status](https://travis-ci.org/axetroy/kost.svg?branch=master)](https://travis-ci.org/axetroy/kost) 4 | [![Coverage Status](https://coveralls.io/repos/github/axetroy/kost/badge.svg?branch=master)](https://coveralls.io/github/axetroy/kost?branch=master) 5 | [![Dependency](https://david-dm.org/axetroy/kost.svg)](https://david-dm.org/axetroy/kost) 6 | ![License](https://img.shields.io/badge/license-MIT-green.svg) 7 | [![Prettier](https://img.shields.io/badge/Code%20Style-Prettier-green.svg)](https://github.com/prettier/prettier) 8 | ![Node](https://img.shields.io/badge/node-%3E=8.9-blue.svg?style=flat-square) 9 | [![npm version](https://badge.fury.io/js/%40axetroy%2Fkost.svg)](https://badge.fury.io/js/%40axetroy%2Fkost) 10 | ![Size](https://github-size-badge.herokuapp.com/axetroy/kost.svg) 11 | 12 | Kost 基于 Koa,使用 Typescript 编写,借鉴于 egg 的**约定大于配置**的思想以及 nest 的依赖注入和装饰器路由。 13 | 14 | 是一款内置多个功能,并遵循一系列规范的 Web 框架 15 | 16 | **特性** 17 | 18 | * [x] 依赖注入 19 | * [x] 使用 Typescript 编写 20 | * [x] 装饰器风格的路由定义 21 | * [x] 支持中间件,包括 Koa 的中间件 22 | * [x] 引入服务的概念 23 | * [x] 支持加载不同环境下的配置文件 24 | * [x] 兼容 Koa 中间件 25 | 26 | **内置特性** 27 | 28 | * [x] Http/Websocket 的代理 29 | * [x] 静态文件服务 30 | * [x] 解析 Http Body 31 | * [x] 视图引擎 32 | * [x] 跨域资源分享 33 | * [ ] 错误捕捉 34 | * [ ] 定时任务 35 | 36 | ### 框架架构 37 | 38 | ![kost](https://raw.githubusercontent.com/axetroy/kost/master/kost.png) 39 | 40 | ### 快速开始 41 | 42 | ```bash 43 | npm install @axetroy/kost --save 44 | ``` 45 | 46 | 这是示例的项目目录, 最简单的搭建一个服务 47 | 48 | ``` 49 | . 50 | ├── app.ts 51 | ├── controllers 52 | │   └── home.controller.ts 53 | └── tsconfig.json 54 | ``` 55 | 56 | ```typescript 57 | // app.ts 58 | import Kost from "@axetroy/kost"; 59 | 60 | const app = new Kost(); 61 | 62 | app 63 | .start() 64 | .then(function(server) { 65 | console.log(`Listen on ${server.address().port}`); 66 | }) 67 | .catch(err => { 68 | console.error(err); 69 | }); 70 | ``` 71 | 72 | ```typescript 73 | // controllers/home.controller.ts 74 | import { Controller, Get } from "@axetroy/kost"; 75 | 76 | export default class HomeController extends Controller { 77 | @Get("/") 78 | index(ctx) { 79 | ctx.body = "hello world"; 80 | } 81 | } 82 | ``` 83 | 84 | ```bash 85 | $ ts-node ./app.ts 86 | ``` 87 | 88 | ## [文档](https://github.com/axetroy/kost/blob/master/doc/useage.md) 89 | 90 | ## Q & A 91 | 92 | Q: 为什么开发这样的框架 93 | 94 | > A: 框架基于以前的项目经验沉淀而来,首先是坚持 Typescript 不动摇,能在开发阶段避免了很多 bug。 95 | 96 | Q: 为什么不使用 nest? 97 | 98 | > A: 因为它是基于 Express,而我以前的项目都是 Typescript + Koa 99 | 100 | Q: 为什么不使用 egg? 101 | 102 | > A: egg 使用 JS 开发,目前对 Typescript 没有一个很好的方案(见识短,没发现),而且 egg 的 service 会丢失类型 IDE 提示,目前 egg 成员已在着手解决这个问题,期待中... 103 | 104 | Q: 与两者的框架区别在哪里? 105 | 106 | > A: 借鉴了 egg 的约定大于配置的思想,约定了一些文件目录,文件名,如果不按照框架写,就会 boom。借鉴了 nest 的 OOP 编程思想,所有的,包括 Controller、Service、Middleware 都是类,都可以进行依赖注入,而且路由定义是装饰器风格,语法糖会让你更加的直观。对于开发而言,会有很好的 IDE 提示。 107 | 108 | Q: 框架内置了一些特性,会不会平白增加性能负担? 109 | 110 | > A: 根据你是否开启特性,来决定是否引入包,所以不会有性能损耗。 111 | 112 | Q: 是否需要配套 CLI 工具? 113 | 114 | > A: 目前没有,编译成 JS 就能运行,可以用 pm2 进行负载均衡。 115 | 116 | Q: 框架是否包含进程管理? 117 | 118 | > A: 框架本身不进行进程管理,没有类似 egg 的 master 主进程管理子进程,没有 agent 119 | 120 | ## 贡献者 121 | 122 | 123 | 124 | | [
Axetroy](http://axetroy.github.io)
[💻](https://github.com/axetroy/kost/commits?author=axetroy) 🔌 [⚠️](https://github.com/axetroy/kost/commits?author=axetroy) [🐛](https://github.com/axetroy/kost/issues?q=author%3Aaxetroy) 🎨 | 125 | | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | 126 | 127 | 128 | 129 | 130 | ## 开源许可协议 131 | 132 | The [MIT License](https://github.com/axetroy/kost/blob/master/LICENSE) 133 | -------------------------------------------------------------------------------- /__test__/app-test-example/controllers/home.controller.ts: -------------------------------------------------------------------------------- 1 | import * as Koa from "koa"; 2 | import { Controller } from "../../../index"; 3 | import { All, Get } from "../../../src/decorators"; 4 | 5 | export default class UserController extends Controller { 6 | @Get("/") 7 | async index(ctx: Koa.Context) { 8 | ctx.body = "hello world"; 9 | } 10 | @Get("/view") 11 | async view(ctx: Koa.Context) { 12 | await ctx.render("index"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /__test__/app-test-example/static/test.text: -------------------------------------------------------------------------------- 1 | hello static text -------------------------------------------------------------------------------- /__test__/app-test-example/views/index.html: -------------------------------------------------------------------------------- 1 |
2 | hello view 3 |
-------------------------------------------------------------------------------- /__test__/config-all-test-example/configs/default.config.yaml: -------------------------------------------------------------------------------- 1 | name: axetroy 2 | env: default -------------------------------------------------------------------------------- /__test__/config-all-test-example/configs/test.config.yaml: -------------------------------------------------------------------------------- 1 | name: axetroy 2 | env: test -------------------------------------------------------------------------------- /__test__/config-default-test-example/configs/default.config.yaml: -------------------------------------------------------------------------------- 1 | name: axetroy -------------------------------------------------------------------------------- /__test__/controller-test-example/controllers/home.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from "../../../index"; 2 | import { All, Get, Use } from "../../../src/decorators"; 3 | 4 | export default class UserController extends Controller { 5 | @All("/") 6 | @Use("logger") 7 | async index(ctx) { 8 | ctx.body = "hello world"; 9 | } 10 | @Get("/name") 11 | async name(ctx) { 12 | ctx.body = "axetroy"; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /__test__/controller-test-example/middlewares/logger.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "../../../index"; 2 | 3 | export default class LoggerMiddleware extends Middleware { 4 | async pipe(ctx, next) { 5 | const before = new Date().getTime(); 6 | await next(); 7 | const after = new Date().getTime(); 8 | const take = after - before; 9 | console.log(`[${ctx.req.method}]: ${ctx.req.url} ${take}ms`); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /__test__/controller-with-invalid-middleware-test-example/controllers/home.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Use } from "../../../index"; 2 | 3 | export default class UserController extends Controller { 4 | @Use("limit") // limit is not a valid middleware, it will throw an error when init 5 | @Get("/") 6 | async index(ctx, next) {} 7 | } 8 | -------------------------------------------------------------------------------- /__test__/controller-with-invalid-middleware-test-example/middlewares/limit.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "../../../index"; 2 | 3 | export default class LimitMiddleware extends Middleware {} 4 | -------------------------------------------------------------------------------- /__test__/invalid-controller-test-example/controllers/home.controller.ts: -------------------------------------------------------------------------------- 1 | // invalid controller 2 | export default class InvalidController {} 3 | -------------------------------------------------------------------------------- /__test__/invalid-controller-test-example/middlewares/logger.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "../../../index"; 2 | 3 | export default class LoggerMiddleware extends Middleware { 4 | async pipe(ctx, next) { 5 | const before = new Date().getTime(); 6 | await next(); 7 | const after = new Date().getTime(); 8 | const take = after - before; 9 | console.log(`[${ctx.req.method}]: ${ctx.req.url} ${take}ms`); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /__test__/invalid-middleware-test-example/controllers/home.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from "../../../index"; 2 | 3 | export default class UserController extends Controller {} 4 | -------------------------------------------------------------------------------- /__test__/invalid-middleware-test-example/middlewares/limit.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "../../../index"; 2 | 3 | export default class LimitMiddleware extends Middleware {} 4 | -------------------------------------------------------------------------------- /__test__/invalid-service-test-example/services/invalid.service.ts: -------------------------------------------------------------------------------- 1 | import { Service } from "../../../index"; 2 | 3 | export default class InvalidService {} 4 | -------------------------------------------------------------------------------- /__test__/middleware-test-example/controllers/home.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from "../../../index"; 2 | import { All, Get } from "../../../src/decorators"; 3 | 4 | export default class UserController extends Controller { 5 | @All("*") 6 | async index(ctx) { 7 | ctx.body = "hello world"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /__test__/middleware-test-example/middlewares/logger.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "../../../index"; 2 | 3 | export default class LoggerMiddleware extends Middleware { 4 | async pipe(ctx, next) { 5 | const before = new Date().getTime(); 6 | await next(); 7 | const after = new Date().getTime(); 8 | const take = after - before; 9 | console.log(`[${ctx.req.method}]: ${ctx.req.url} ${take}ms`); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /__test__/service-test-example/services/log.service.ts: -------------------------------------------------------------------------------- 1 | import { Service } from "../../../index"; 2 | 3 | export default class LogService extends Service { 4 | level = 100; // log service should be init first 5 | public initedAt: Date; 6 | async init() { 7 | this.initedAt = new Date(); 8 | 9 | // do some job 10 | await new Promise((resolve, reject) => { 11 | setTimeout(() => { 12 | resolve(); 13 | }, 10); 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /__test__/service-test-example/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Service } from "../../../index"; 2 | 3 | export default class UserService extends Service { 4 | public username: string; 5 | public initedAt: Date; 6 | async getUser() { 7 | return { 8 | name: "Axetroy" 9 | }; 10 | } 11 | async init() { 12 | this.username = "admin"; 13 | 14 | this.initedAt = new Date(); 15 | 16 | // do some job 17 | await new Promise((resolve, reject) => { 18 | setTimeout(() => { 19 | resolve(); 20 | }, 10); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to axetroy 2 | 3 | First and foremost, thank you! We appreciate that you want to contribute to axetroy, your time is valuable, and your contributions mean a lot to us. 4 | 5 | **What does "contributing" mean?** 6 | 7 | Creating an issue is the simplest form of contributing to a project. But there are many ways to contribute, including the following: 8 | 9 | - Updating or correcting documentation 10 | - Feature requests 11 | - Bug reports 12 | 13 | If you'd like to learn more about contributing in general, the [Guide to Idiomatic Contributing](https://github.com/jonschlinkert/idiomatic-contributing) has a lot of useful information. 14 | 15 | **Showing support for axetroy** 16 | 17 | Please keep in mind that open source software is built by people like you, who spend their free time creating things the rest the community can use. 18 | 19 | Don't have time to contribute? No worries, here are some other ways to show your support for axetroy: 20 | 21 | - star the [project](https://github.com/axetroy/kost#readme) 22 | - tweet your support for axetroy 23 | 24 | ## Issues 25 | 26 | ### Before creating an issue 27 | 28 | Please try to determine if the issue is caused by an underlying library, and if so, create the issue there. Sometimes this is difficult to know. We only ask that you attempt to give a reasonable attempt to find out. Oftentimes the readme will have advice about where to go to create issues. 29 | 30 | Try to follow these guidelines 31 | 32 | - **Investigate the issue**: 33 | - **Check the readme** - oftentimes you will find notes about creating issues, and where to go depending on the type of issue. 34 | - Create the issue in the appropriate repository. 35 | 36 | ### Creating an issue 37 | 38 | Please be as descriptive as possible when creating an issue. Give us the information we need to successfully answer your question or address your issue by answering the following in your issue: 39 | 40 | - **version**: please note the version of axetroy are you using 41 | - **extensions, plugins, helpers, etc** (if applicable): please list any extensions you're using 42 | - **error messages**: please paste any error messages into the issue, or a [gist](https://gist.github.com/) 43 | 44 | ## Above and beyond 45 | 46 | Here are some tips for creating idiomatic issues. Taking just a little bit extra time will make your issue easier to read, easier to resolve, more likely to be found by others who have the same or similar issue in the future. 47 | 48 | - read the [Guide to Idiomatic Contributing](https://github.com/jonschlinkert/idiomatic-contributing) 49 | - take some time to learn basic markdown. This [markdown cheatsheet](https://gist.github.com/jonschlinkert/5854601) is super helpful, as is the GitHub guide to [basic markdown](https://help.github.com/articles/markdown-basics/). 50 | - Learn about [GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/). And if you want to really go above and beyond, read [mastering markdown](https://guides.github.com/features/mastering-markdown/). 51 | - use backticks to wrap code. This ensures that code will retain its format, making it much more readable to others 52 | - use syntax highlighting by adding the correct language name after the first "code fence" 53 | 54 | 55 | [node-glob]: https://github.com/isaacs/node-glob 56 | [micromatch]: https://github.com/jonschlinkert/micromatch 57 | [so]: http://stackoverflow.com/questions/tagged/axetroy 58 | -------------------------------------------------------------------------------- /doc/useage.md: -------------------------------------------------------------------------------- 1 | # Document 2 | 3 | * [快速开始](#快速开始) 4 | 5 | * [一系列规范和约定](#一系列规范和约定) 6 | 7 | * [文件目录](#文件目录) 8 | * [Config](#config-约定) 9 | * [Controller](#controller-约定) 10 | * [Service](#service-约定) 11 | * [Middleware](#middleware-约定) 12 | 13 | * [加载配置](#加载配置) 14 | 15 | * [内置特性](#build-in-feature) 16 | 17 | * [Http/Websocket 代理](#代理) 18 | * [静态文件服务](#静态文件服务) 19 | * [Body Parser](#body-parser) 20 | * [模版渲染](#模版渲染) 21 | * [跨域资源分享](#跨域资源分享) 22 | 23 | * [Controller](#controller) 24 | 25 | * [如何编写一个 Controller?](#如何编写一个-controller) 26 | * [如何在 Controller 中使用 Service?](#如何在-controller-中使用-service) 27 | * [如何在 Controller 中获取框架的上下文 Context?](#如何在-controller-中获取框架的上下文-context) 28 | 29 | * [Middleware](#middleware) 30 | 31 | * [如何编写一个 Middleware?](#如何编写一个-middleware) 32 | * [如何复用或兼容 Koa 的中间件?](#如何复用或兼容-koa-的中间件) 33 | * [中间件怎么运用到全局请求?](#中间件怎么运用到全局请求) 34 | * [如何针对某个 API 使用 Middleware?](#如何针对某个-api-使用-middleware) 35 | 36 | * [Service](#service) 37 | 38 | * [如何编写一个 Service?](#如何编写一个-service) 39 | * [如何使用 Service?](#如何使用-service) 40 | * [如何初始化服务?](#如何初始化服务) 41 | 42 | * [Context](#context) 43 | 44 | ## 快速开始 45 | 46 | 这是示例的项目目录, 最简单的搭建一个服务 47 | 48 | ``` 49 | . 50 | ├── app.ts 51 | ├── controllers 52 | │   └── home.controller.ts 53 | └── tsconfig.json 54 | ``` 55 | 56 | ```typescript 57 | // app.ts 58 | import Kost from "@axetroy/kost"; 59 | 60 | const app = new Kost(); 61 | 62 | app 63 | .start() 64 | .then(function(server) { 65 | console.log(`Listen on ${server.address().port}`); 66 | }) 67 | .catch(err => { 68 | console.error(err); 69 | }); 70 | ``` 71 | 72 | ```typescript 73 | // controllers/home.controller.ts 74 | import { Controller, Get } from "@axetroy/kost"; 75 | 76 | export default class HomeController extends Controller { 77 | @Get("/") 78 | index(ctx) { 79 | ctx.body = "hello world"; 80 | } 81 | } 82 | ``` 83 | 84 | ```bash 85 | $ ts-node ./app.ts 86 | ``` 87 | 88 | ## 一系列规范和约定 89 | 90 | 框架的正常运行,依赖与一系列的规范和预定 91 | 92 | 其中包括: 93 | 94 | ### 文件目录 95 | 96 | 约定几个固定的文件目录 97 | 98 | * configs: 存放配置文件 99 | * controllers: 存放控制器 100 | * services: 存放服务 101 | * middlewares: 存放中间件 102 | * static: 存放静态文件 103 | * views: 存放视图模板 104 | 105 | ### Config 约定 106 | 107 | 控制器约定要放在`项目目录/configs`下,并且以为`xx.config.yaml`命名, 目前仅支持 yaml 文件作为配置,不支持 json 108 | 109 | 各配置文件中的字段,应该一致 110 | 111 | ### Controller 约定 112 | 113 | 控制器约定要放在`项目目录/controllers`下,并且以为`xx.controller.ts`命名 114 | 115 | 控制器文件暴露一个继承自 Controller 的类 116 | 117 | ### Service 约定 118 | 119 | 控制器约定要放在`项目目录/services`下,并且以为`xx.service.ts`命名 120 | 121 | 控制器文件暴露一个继承自 Service 的类 122 | 123 | 可以通过定义`async init()`来初始化 service 124 | 125 | 定义`level`属性来排序服务之间的初始化顺序 126 | 127 | ### Middleware 约定 128 | 129 | 中间件约定要放在`项目目录/middlewares`下,并且以为`xx.middleware.ts`命名 130 | 131 | 控制器文件暴露一个继承自 Middleware 的类,并且必须实现`async pipe(ctx, next)`方法 132 | 133 | ## 加载配置 134 | 135 | 框架根据环境变量`NODE_ENV`,自动加载 yaml 配置文件 136 | 137 | 配置文件必须放在`/configs`目录下,例如`/configs/development.config.yaml` 138 | 139 | 默认会加载`default.config.yaml` 140 | 141 | 然后会与`${process.env.NODE_ENV}.config.yaml`的配置文件合并在一起 142 | 143 | 你可以通过[Context](#context)获取配置文件 144 | 145 | ## 内置特性 146 | 147 | ### 代理 148 | 149 | 内置了[koa-proxies](https://github.com/vagusX/koa-proxies)该特性提供代理 http 或 Websocket,做到请求转发。 150 | 151 | 下面一个例子是代理`{host}/proxy` 到 `http://127.0.0.1:3000` 152 | 153 | ``` 154 | localhost:3000/proxy > http://127.0.0.1:3000 155 | localhost:3000/proxy/user/axetroy > http://127.0.0.1:3000/user/axetroy 156 | ``` 157 | 158 | ```typescript 159 | import Application from "@axetroy/kost"; 160 | 161 | new Application() 162 | .start({ 163 | enabled: { 164 | proxy: { 165 | mount: "/proxy", 166 | options: { 167 | target: "http://127.0.0.1:3000", 168 | changeOrigin: true, 169 | xfwd: true, 170 | cookieDomainRewrite: true, 171 | proxyTimeout: 1000 * 120, 172 | logs: true 173 | } 174 | } 175 | } 176 | }) 177 | .catch(function(err) { 178 | console.error(err); 179 | }); 180 | ``` 181 | 182 | ### 静态文件服务 183 | 184 | 内置了[koa-static](https://github.com/koajs/static) 提供静态文件服务 185 | 186 | 默认提供的 url 路径为`{host}/static` 187 | 188 | ```typescript 189 | import Application from "@axetroy/kost"; 190 | 191 | new Application() 192 | .start({ 193 | enabled: { 194 | static: true // or you can pass an object, see https://github.com/koajs/static#options 195 | } 196 | }) 197 | .catch(function(err) { 198 | console.error(err); 199 | }); 200 | ``` 201 | 202 | ### Body Parser 203 | 204 | 内置了[koa-bodyparser](https://github.com/koajs/bodyparser), 用于解析请求的 Http Body 205 | 206 | 设置 `true` 则使用默认配置 207 | 208 | ```typescript 209 | import Application from "@axetroy/kost"; 210 | 211 | new Application() 212 | .start({ 213 | enabled: { 214 | bodyParser: true // 或者你可以传入一个对象, 详情 https://github.com/koajs/bodyparser#options 215 | } 216 | }) 217 | .catch(function(err) { 218 | console.error(err); 219 | }); 220 | ``` 221 | 222 | ### 模版渲染 223 | 224 | 首先确保 `/views` 目录存在, 所有的模版都应该存放在这里目录下. 225 | 226 | 内置了[koa-views](https://github.com/queckezz/koa-views),提供模版引擎 227 | 228 | 设置 `true` 则使用默认配置 229 | 230 | ```typescript 231 | import Application from "@axetroy/kost"; 232 | 233 | new Application() 234 | .start({ 235 | enabled: { 236 | view: true // 或者你可以传入一个对象, 详情 https://github.com/queckezz/koa-views#viewsroot-opts 237 | } 238 | }) 239 | .catch(function(err) { 240 | console.error(err); 241 | }); 242 | ``` 243 | 244 | #### 跨域资源分享 245 | 246 | 内置了[koa-cors](https://github.com/evert0n/koa-cors/)提供跨域资源的分享 247 | 248 | 设置 `true` 则使用默认配置 249 | 250 | ```typescript 251 | import Application from "@axetroy/kost"; 252 | 253 | new Application() 254 | .start({ 255 | enabled: { 256 | cors: true // 或者你可以传入一个对象, 详情 https://github.com/evert0n/koa-cors/#options 257 | } 258 | }) 259 | .catch(function(err) { 260 | console.error(err); 261 | }); 262 | ``` 263 | 264 | ## Controller 265 | 266 | Controller 是一个类,在框架初始化时自动实例化,它定义了你如何组织你的 API。 267 | 268 | 在控制器上,你可以使用`@Get()`、`@Post()`、`@Put()`等装饰器来定义你的路由 269 | 270 | 也可以使用`@Use()`来指定某个 API 加载指定的中间件 271 | 272 | 也可以使用`@Inject()`来注入服务 273 | 274 | ### 如何编写一个 Controller? 275 | 276 | 框架规定控制器必须在`/controllers`目录下。并且命名为`xxx.controller.ts` 277 | 278 | 下面是一个例子 279 | 280 | ```typescript 281 | // controllers/user.ts 282 | 283 | import { Controller, GET, POST, DELETE } from "@axetroy/kost"; 284 | 285 | class UserController extends Controller { 286 | @GET("/") 287 | async index(ctx, next) { 288 | ctx.body = "hello kost"; 289 | } 290 | @POST("/login") 291 | async login(ctx, next) { 292 | ctx.body = "login success"; 293 | } 294 | @DELETE("/logout") 295 | async logout(ctx, next) { 296 | ctx.body = "logout success"; 297 | } 298 | } 299 | 300 | export default UserController; 301 | ``` 302 | 303 | ### 如何在 Controller 中使用 Service? 304 | 305 | 假设你已经定义了一个服务 306 | 307 | ```typescript 308 | // services/user.service.ts 309 | import { Service } from "@axetroy/kost"; 310 | 311 | class UserService extends Service { 312 | async getUser(username: string) { 313 | return { 314 | name: username, 315 | age: 21, 316 | address: "unknown" 317 | }; 318 | } 319 | } 320 | 321 | export default UserService; 322 | ``` 323 | 324 | 那么你需要这么使用, 通过装饰器 `@Inject()` 注入你所需要的服务 325 | 326 | ```typescript 327 | // controllers/user.ts 328 | 329 | import { Controller, GET, Inject } from "@axetroy/kost"; 330 | import UserService from "../services/user.service"; 331 | 332 | class UserController extends Controller { 333 | @Inject() user: UserService; 334 | @GET("/:username") 335 | async getUserInfo(ctx, next) { 336 | const userInfo = await this.user.getUserInfo(ctx.params.username); 337 | ctx.body = userInfo; 338 | } 339 | } 340 | 341 | export default UserController; 342 | ``` 343 | 344 | ### 如何在 Controller 中获取框架的上下文 Context? 345 | 346 | 框架的上下文包括了一些非常有用的信息 347 | 348 | * [x] 项目的配置 349 | * [x] 框架的启动参数 350 | 351 | 与注入服务一样,使用 `@Inject()` 注入[Context](#context) 352 | 353 | ```typescript 354 | // controllers/user.controller.ts 355 | 356 | import { Controller, GET, Inject, Context } from "@axetroy/kost"; 357 | 358 | class UserController extends Controller { 359 | @Inject() context: Context; 360 | @GET("/context") 361 | async getTheAppContext(ctx, next) { 362 | ctx.body = this.context; 363 | } 364 | } 365 | 366 | export default UserController; 367 | ``` 368 | 369 | ## Middleware 370 | 371 | Middleware 是 Koa 中间件的一层 OOP 的封装 372 | 373 | 它要求你必须实现`pipe(ctx, next)`方法 374 | 375 | 中间件可用于全局,或者某个局部的 API,它支持从`middlewares/xxx.middleware.ts`中加载,也支持从`node_modules`中加载 376 | 377 | 优先级: 本地 > npm 378 | 379 | ### 如何编写一个 Middleware? 380 | 381 | 框架规定中间件必须在`/middlewares`目录下。并且命名为`xxx.middleware.ts` 382 | 383 | 下面是一个例子 384 | 385 | ```typescript 386 | // middlewares/logger.middleware.ts 387 | 388 | import { Middleware } from "@axetroy/kost"; 389 | export default class extends Middleware { 390 | async pipe(ctx, next) { 391 | const before = new Date().getTime(); 392 | await next(); 393 | const after = new Date().getTime(); 394 | const take = after - before; 395 | console.log(`[${ctx.req.method}]: ${ctx.req.url} ${take}ms`); 396 | } 397 | } 398 | ``` 399 | 400 | ### 如何复用或兼容 Koa 的中间件? 401 | 402 | 如果你想直接使用 Koa 的中间件, 例如 [koa-cors](https://github.com/evert0n/koa-cors) 403 | 404 | 在项目目录下,创建一个文件 `middlewares/cors.middleware.ts` 405 | 406 | ```typescript 407 | // /middlewares/cors.middleware.ts 408 | 409 | import { Middleware } from "@axetroy/kost"; 410 | import * as cors from "koa-cors"; 411 | export default class extends Middleware { 412 | async pipe(ctx, next) { 413 | return cors({ 414 | origin: "*" 415 | }); 416 | } 417 | } 418 | ``` 419 | 420 | ### 中间件怎么运用到全局请求? 421 | 422 | 加入你已经创建了一个中间件`middlewares/logger.middleware.ts` 423 | 424 | 你可以这么使用 425 | 426 | ```typescript 427 | // app.ts 428 | import Kost from "@axetroy/kost"; 429 | 430 | new Kost() 431 | .use("logger", {}) 432 | .use("another middleware", {}) 433 | .start() 434 | .catch(function(err) { 435 | console.error(err); 436 | }); 437 | ``` 438 | 439 | **NOTE**: `use()`方法同样兼容 Koa 中间件,并且支持从`node_modules`中加载 440 | 441 | ### 如何针对某个 API 使用 Middleware? 442 | 443 | 下面是一个例子,通过使用`@Use()`装饰器,指定某个 API 的中间件 444 | 445 | ```typescript 446 | // /project/controllers/user.ts 447 | 448 | import { Controller, Get, Use } from "@axetroy/kost"; 449 | 450 | class UserController extends Controller { 451 | @Get("/") 452 | @Use("logger", {}) 453 | async index(ctx, next) { 454 | ctx.body = "hello kost"; 455 | } 456 | } 457 | 458 | export default UserController; 459 | ``` 460 | 461 | ## Service 462 | 463 | 服务是一个类,能够被注入到 Controller 当中,甚至可以注入到其他的 Service,也能够注入到 Middleware 中 464 | 465 | 使用`@Inject()`装饰器,你能够注入到任何你需要的地方 466 | 467 | 它跟 Controller 有什么区别? 468 | 469 | * [x] Service 是一个可注入的类, 你可以注入到 Controller/Service/Middleware 470 | * [x] Service 的逻辑是可以公用的,而 Controller 不能公用. 471 | * [x] Service 可以被初始化, 按照`level`属性进行排序,level 越高,优先级越高 472 | 473 | ### 如何编写一个 Service? 474 | 475 | 框架规定中间件必须在`/services`目录下。并且命名为`xxx.service.ts` 476 | 477 | 下面是一个例子 478 | 479 | ```typescript 480 | // services/user.service.ts 481 | import { Service } from "@axetroy/kost"; 482 | 483 | class UserService extends Service { 484 | async getUser(username: string) { 485 | return { 486 | name: username, 487 | age: 21, 488 | address: "unknown" 489 | }; 490 | } 491 | } 492 | 493 | export default UserService; 494 | ``` 495 | 496 | ### 如何使用 Service? 497 | 498 | 服务可以通过下列方式使用: 499 | 500 | 1. 注入到 Controller 501 | 2. 注入到其他 Service 502 | 3. 注入到 Middleware 503 | 504 | 下面这个例子展示了如何在 Controller 中使用 505 | 506 | ```typescript 507 | // controllers/user.ts 508 | 509 | import { Controller, Get, Inject } from "@axetroy/kost"; 510 | import UserService from "../services/user"; 511 | 512 | class UserController extends Controller { 513 | @Inject() user: UserService; 514 | @Get("/:username") 515 | async getUserInfo(ctx, next) { 516 | const userInfo = await this.user.getUserInfo(ctx.params.username); 517 | ctx.body = userInfo; 518 | } 519 | } 520 | 521 | export default UserController; 522 | ``` 523 | 524 | ### 如何初始化服务? 525 | 526 | 有一些服务,在使用前,需要做异步的初始化动作 527 | 528 | 你可以声明 Service 的`level`属性和`async init()`方法来初始化 529 | 530 | 例如,我们又下列两个方法来初始化 531 | 532 | * /project/services/orm.service.ts 533 | * /project/services/user.service.ts 534 | 535 | 如果你想先初始化`orm.service.ts`再初始化`user.service.ts` 536 | 537 | 你可以这样定义 Service 的`level` 538 | 539 | ```typescript 540 | // services/orm.service.ts 541 | import { Service } from "@axetroy/kost"; 542 | 543 | class OrmService extends Service { 544 | level: 100; // set the level for this service, default: 0 545 | async init() { 546 | // create an connection for database 547 | } 548 | async query(sql: string) { 549 | // do something job 550 | } 551 | } 552 | 553 | export default OrmService; 554 | ``` 555 | 556 | ```typescript 557 | // services/user.service.ts 558 | import { Service } from "@axetroy/kost"; 559 | 560 | class UserService extends Service { 561 | level: 99; // set the level for this service, default: 0 562 | async init() { 563 | // create user if doest not exist 564 | } 565 | async createUser(sql: string) { 566 | // do something job 567 | } 568 | } 569 | 570 | export default InitUserService; 571 | ``` 572 | 573 | ## Context 574 | 575 | What's context? 576 | 577 | 什么是 Context? 578 | 579 | Context 是框架的执行上下文,其中包含了重要的信息, 包括[配置](#配置加载), 包括启动参数. 580 | 581 | Context 是一个可注入的类,它可以注入到任何地方,包括 Controller/Service/Middleware 582 | 583 | Context 不需要你实例化,手动实例化会引发错误 584 | -------------------------------------------------------------------------------- /example/advanced/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /example/advanced/app.ts: -------------------------------------------------------------------------------- 1 | import Application from "../../index"; 2 | 3 | new Application({ 4 | enabled: { 5 | cors: true, 6 | static: true, 7 | proxy: { 8 | mount: "/proxy", 9 | options: { 10 | target: "http://127.0.0.1:3000", 11 | changeOrigin: true, 12 | xfwd: true, 13 | cookieDomainRewrite: true, 14 | proxyTimeout: 1000 * 120, // 2分钟为超时 15 | logs: true 16 | } 17 | }, 18 | bodyParser: true, 19 | view: true 20 | } 21 | }) 22 | .use("logger") 23 | .start(3000) 24 | .then(function(server) { 25 | console.log(`Listen on ${server.address().port}`); 26 | }) 27 | .catch(function(err) { 28 | console.error(err); 29 | }); 30 | -------------------------------------------------------------------------------- /example/advanced/configs/default.config.yaml: -------------------------------------------------------------------------------- 1 | env: development 2 | database: 3 | host: localhost 4 | port: 2345 -------------------------------------------------------------------------------- /example/advanced/configs/development.config.yaml: -------------------------------------------------------------------------------- 1 | env: development 2 | database: 3 | host: 1234 4 | port: 2345 -------------------------------------------------------------------------------- /example/advanced/configs/production.config.yaml: -------------------------------------------------------------------------------- 1 | database: 2 | host: localhost 3 | port: 2345 -------------------------------------------------------------------------------- /example/advanced/configs/test.config.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axetroy/kost/000c5153895c0c29e482c7bc75e623172c819e81/example/advanced/configs/test.config.yaml -------------------------------------------------------------------------------- /example/advanced/controllers/todo.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Inject, Get, Post } from "../../../index"; 2 | 3 | import UserService from "../services/user.service"; 4 | 5 | export default class TodoController extends Controller { 6 | @Inject() user: UserService; 7 | 8 | @Get("/todo/list") 9 | index(ctx, next) { 10 | ctx.body = [ 11 | { 12 | name: "Shopping", 13 | state: "done" 14 | }, 15 | { 16 | name: "Buy a house", 17 | state: "undone" 18 | } 19 | ]; 20 | } 21 | @Post("/todo/create") 22 | async name(ctx, next) { 23 | ctx.body = "create a new todo task"; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /example/advanced/controllers/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Inject, Get, Use, Context } from "../../../index"; 2 | 3 | import UserService from "../services/user.service"; 4 | 5 | class UserController extends Controller { 6 | @Inject() user: UserService; 7 | @Inject() context: Context; 8 | 9 | @Get("/") 10 | index(ctx, next) { 11 | ctx.body = "hello world"; 12 | } 13 | @Get("/hello") 14 | async say(ctx, next) { 15 | await ctx.render("index.html"); 16 | } 17 | @Get("/whoami") 18 | async name(ctx, next) { 19 | ctx.body = await this.user.getUser(); 20 | } 21 | @Get(/^\/user\/\w/gi) 22 | @Use("logger") 23 | async info(ctx, next) { 24 | console.log(this.context); 25 | ctx.body = "regular expression match"; 26 | } 27 | } 28 | 29 | export default UserController; 30 | -------------------------------------------------------------------------------- /example/advanced/middlewares/logger.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "./../../../index"; 2 | 3 | export default class LoggerMiddleware extends Middleware { 4 | async pipe(ctx, next) { 5 | const before = new Date().getTime(); 6 | await next(); 7 | const after = new Date().getTime(); 8 | const take = after - before; 9 | console.log(`[${ctx.req.method}]: ${ctx.req.url} ${take}ms`); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /example/advanced/services/orm.service.ts: -------------------------------------------------------------------------------- 1 | import { Service, Inject } from "../../../index"; 2 | 3 | class OrmService extends Service { 4 | async getUser() { 5 | return { 6 | name: "axetroy", 7 | age: 21 8 | }; 9 | } 10 | async init() { 11 | console.log("初始化数据库连接..."); 12 | } 13 | } 14 | 15 | export default OrmService; 16 | -------------------------------------------------------------------------------- /example/advanced/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Service, Inject } from "../../../index"; 2 | 3 | class UserService extends Service { 4 | async getUser() { 5 | return { 6 | name: "axetroy", 7 | age: 21 8 | }; 9 | } 10 | async init() { 11 | console.log("创建一个默认账户..."); 12 | } 13 | } 14 | 15 | export default UserService; 16 | -------------------------------------------------------------------------------- /example/advanced/static/test.text: -------------------------------------------------------------------------------- 1 | hello text -------------------------------------------------------------------------------- /example/advanced/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "ES2017", 5 | /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ 6 | "module": "commonjs", 7 | /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 8 | "lib": ["dom", "es2015", "es2016", "es2017", "esnext"], 9 | /* Specify library files to be included in the compilation: */ 10 | "allowJs": true /* Allow javascript files to be compiled. */, 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | "sourceMap": true /* Generates corresponding '.map' file. */, 15 | // "outFile": "./", /* Concatenate and emit output to single file. */ 16 | "outDir": "./build" /* Redirect output structure to the directory. */, 17 | // "rootDir": 18 | // "example" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 19 | "removeComments": true /* Do not emit comments to output. */, 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | /* Strict Type-Checking Options */ 25 | "strict": true, 26 | /* Enable all strict type-checking options. */ 27 | "noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */, 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | "noImplicitThis": false /* Raise error on 'this' expressions with an implied 'any' type. */, 30 | "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 31 | 32 | /* Additional Checks */ 33 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 34 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 35 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 36 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 37 | 38 | /* Module Resolution Options */ 39 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 40 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 41 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 42 | "rootDirs": [ 43 | "./" 44 | ] /* List of root folders whose combined content represents the structure of the project at runtime. */, 45 | "typeRoots": ["node_modules/@types"], 46 | /* List of folders to include type definitions from. */ 47 | "types": ["node"], 48 | /* Type declaration files to be included in compilation. */ 49 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 50 | 51 | /* Source Map Options */ 52 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 53 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 54 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 55 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 56 | 57 | /* Experimental Options */ 58 | "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, 59 | "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */ 60 | }, 61 | "include": ["**/*"], 62 | "exclude": ["node_modules"] 63 | } 64 | -------------------------------------------------------------------------------- /example/advanced/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | hello html view 11 | 12 | -------------------------------------------------------------------------------- /example/basic/app.ts: -------------------------------------------------------------------------------- 1 | import Kost from "../../index"; 2 | 3 | const app = new Kost(); 4 | 5 | app 6 | .start() 7 | .then(function(server) { 8 | console.log(`Listen on ${server.address().port}`); 9 | }) 10 | .catch(err => { 11 | console.error(err); 12 | }); 13 | -------------------------------------------------------------------------------- /example/basic/controllers/home.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from "../../../index"; 2 | 3 | export default class HomeController extends Controller { 4 | @Get("/") 5 | index(ctx) { 6 | ctx.body = "hello world"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /example/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "ES2017", 5 | /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ 6 | "module": "commonjs", 7 | /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 8 | "lib": ["dom", "es2015", "es2016", "es2017", "esnext"], 9 | /* Specify library files to be included in the compilation: */ 10 | "allowJs": true /* Allow javascript files to be compiled. */, 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | "sourceMap": true /* Generates corresponding '.map' file. */, 15 | // "outFile": "./", /* Concatenate and emit output to single file. */ 16 | "outDir": "./build" /* Redirect output structure to the directory. */, 17 | // "rootDir": 18 | // "example" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 19 | "removeComments": true /* Do not emit comments to output. */, 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | /* Strict Type-Checking Options */ 25 | "strict": true, 26 | /* Enable all strict type-checking options. */ 27 | "noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */, 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | "noImplicitThis": false /* Raise error on 'this' expressions with an implied 'any' type. */, 30 | "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 31 | 32 | /* Additional Checks */ 33 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 34 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 35 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 36 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 37 | 38 | /* Module Resolution Options */ 39 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 40 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 41 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 42 | "rootDirs": [ 43 | "./" 44 | ] /* List of root folders whose combined content represents the structure of the project at runtime. */, 45 | "typeRoots": ["node_modules/@types"], 46 | /* List of folders to include type definitions from. */ 47 | "types": ["node"], 48 | /* Type declaration files to be included in compilation. */ 49 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 50 | 51 | /* Source Map Options */ 52 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 53 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 54 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 55 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 56 | 57 | /* Experimental Options */ 58 | "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, 59 | "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */ 60 | }, 61 | "include": ["**/*"], 62 | "exclude": ["node_modules"] 63 | } 64 | -------------------------------------------------------------------------------- /index.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import * as path from "path"; 3 | import Kost from "./index"; 4 | import * as request from "supertest"; 5 | import { paths, setCurrentWorkingDir } from "./src/path"; 6 | 7 | test("index", async t => { 8 | // default mode 9 | t.deepEqual(process.env.NODE_ENV, "test"); 10 | }); 11 | 12 | test("basic", async t => { 13 | setCurrentWorkingDir( 14 | path.join(process.cwd(), "build", "__test__", "controller-test-example") 15 | ); 16 | 17 | const app = new Kost(); 18 | 19 | await (app).init(); 20 | 21 | const server = request(app.callback()); 22 | 23 | const res1: any = await new Promise((resolve, reject) => { 24 | server.get("/").end((err, res) => (err ? reject(err) : resolve(res))); 25 | }); 26 | 27 | t.deepEqual(res1.text, "hello world"); 28 | 29 | const res2: any = await new Promise((resolve, reject) => { 30 | server.get("/name").end((err, res) => (err ? reject(err) : resolve(res))); 31 | }); 32 | 33 | t.deepEqual(res2.text, "axetroy"); 34 | }); 35 | 36 | test("invalid middleware", async t => { 37 | let app; 38 | t.notThrows(function() { 39 | // hello.middleware.ts is not exist at all 40 | // even thought, before it start, it will not throw; 41 | app = new Kost().use("hello"); 42 | }); 43 | 44 | // when init, it should throw error 45 | try { 46 | await app.init(); 47 | } catch (err) { 48 | t.true(err instanceof Error); 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || "development"; 2 | 3 | import { Inject } from "typedi"; 4 | 5 | import Application from "./src/app"; 6 | import Controller from "./src/class/controller"; 7 | import Middleware from "./src/class/middleware"; 8 | import Service from "./src/class/service"; 9 | import Context from "./src/class/context"; 10 | export * from "./src/decorators"; 11 | 12 | export default Application; 13 | 14 | export { Controller, Service, Middleware, Context, Inject }; 15 | -------------------------------------------------------------------------------- /kost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axetroy/kost/000c5153895c0c29e482c7bc75e623172c819e81/kost.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@axetroy/kost", 3 | "version": "0.1.0", 4 | "description": "The web framework base on Koa and Typescript for NodeJS", 5 | "main": "build/index.js", 6 | "directories": { 7 | "example": "example" 8 | }, 9 | "devDependencies": { 10 | "@types/fs-extra": "^5.0.0", 11 | "@types/http-proxy": "^1.12.4", 12 | "@types/js-yaml": "^3.10.1", 13 | "@types/koa": "^2.0.44", 14 | "@types/koa-bodyparser": "^4.2.0", 15 | "@types/koa-router": "^7.0.27", 16 | "@types/koa-views": "^2.0.3", 17 | "@types/node": "^9.4.5", 18 | "@types/supertest": "^2.0.4", 19 | "ava": "^0.25.0", 20 | "coveralls": "^3.0.0", 21 | "glob": "^7.1.2", 22 | "nyc": "^11.4.1", 23 | "supertest": "^3.0.0", 24 | "ts-node": "^5.0.0", 25 | "typescript": "^2.7.1" 26 | }, 27 | "dependencies": { 28 | "fs-extra": "^5.0.0", 29 | "https-proxy-agent": "^2.1.1", 30 | "js-yaml": "^3.10.0", 31 | "koa": "^2.5.0", 32 | "koa-bodyparser": "^4.2.0", 33 | "koa-cors": "^0.0.16", 34 | "koa-mount": "^3.0.0", 35 | "koa-proxies": "^0.6.2", 36 | "koa-router": "^7.4.0", 37 | "koa-static": "^4.0.2", 38 | "koa-views": "^6.1.3", 39 | "reflect-metadata": "^0.1.12", 40 | "typedi": "^0.7.0" 41 | }, 42 | "scripts": { 43 | "test": "rm -rf ./build && tsc -p ./ && node scripts/test.js && nyc ava ./build/**/*.test.js ./build/**/**/*.test.js", 44 | "build": "rm -rf ./build && tsc -p ./ -d" 45 | }, 46 | "author": "Axetroy", 47 | "license": "MIT" 48 | } 49 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by axetroy on 2017/7/23. 3 | */ 4 | const path = require("path"); 5 | const glob = require("glob"); 6 | const fs = require("fs-extra"); 7 | 8 | const asset = { 9 | ".json": true, 10 | ".yaml": true, 11 | ".yml": true, 12 | ".html": true, 13 | ".hbs": true, 14 | ".text": true 15 | }; 16 | 17 | glob("__test__/**/**/*", function(err, files) { 18 | if (err) throw err; 19 | while (files.length) { 20 | const file = files.shift(); 21 | const f = path.parse(file); 22 | 23 | if (file.indexOf("node_modules") >= 0) { 24 | return; 25 | } 26 | 27 | if (asset[f.ext]) { 28 | const distFile = file.replace(/^__test__/, "build/__test__"); 29 | fs.copy(file, distFile).catch(err => { 30 | console.error(err); 31 | }); 32 | } 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /src/app.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import * as Koa from "koa"; 3 | import Application from "./app"; 4 | import * as path from "path"; 5 | import {setCurrentWorkingDir} from "./path"; 6 | import * as request from "supertest"; 7 | 8 | test("app", async t => { 9 | t.deepEqual(new Application() instanceof Koa, true); 10 | }); 11 | 12 | test.serial("app start with default build-in feature", async t => { 13 | setCurrentWorkingDir( 14 | path.join(process.cwd(), "build", "__test__", "app-test-example") 15 | ); 16 | 17 | const app = new Application({ 18 | enabled: { 19 | cors: true, 20 | static: true, 21 | view: true, 22 | bodyParser: true, 23 | proxy: { 24 | mount: "/proxy", 25 | options: { 26 | target: "http://localhost:3000", 27 | changeOrigin: true, 28 | xfwd: true, 29 | cookieDomainRewrite: true, 30 | proxyTimeout: 1000 * 120, 31 | logs: true 32 | } 33 | } 34 | } 35 | }); 36 | 37 | const server = await app.start(9077); 38 | server.close(); 39 | t.pass(); 40 | }); 41 | 42 | test("app start with default build-in feature", async t => { 43 | setCurrentWorkingDir( 44 | path.join(process.cwd(), "build", "__test__", "app-test-example") 45 | ); 46 | 47 | const app = new Application({ 48 | enabled: { 49 | cors: true, 50 | static: true, 51 | view: true, 52 | bodyParser: true, 53 | proxy: { 54 | mount: "/proxy", 55 | options: { 56 | target: "http://www.baidu.com", 57 | changeOrigin: true, 58 | xfwd: true, 59 | cookieDomainRewrite: true, 60 | proxyTimeout: 1000 * 120, 61 | logs: true 62 | } 63 | } 64 | } 65 | }); 66 | 67 | await (app).init(); 68 | 69 | const server = request(app.callback()); 70 | 71 | const res: any = await new Promise((resolve, reject) => { 72 | server.get("/").end((err, res) => (err ? reject(err) : resolve(res))); 73 | }); 74 | 75 | t.deepEqual(res.text, "hello world"); 76 | 77 | // request view 78 | const res2: any = await new Promise((resolve, reject) => { 79 | server.get("/view").end((err, res) => (err ? reject(err) : resolve(res))); 80 | }); 81 | 82 | t.deepEqual( 83 | res2.text, 84 | `
85 | hello view 86 |
` 87 | ); 88 | 89 | t.deepEqual(res2.header["access-control-allow-origin"], "*"); 90 | t.deepEqual( 91 | res2.header["access-control-allow-methods"], 92 | "GET,HEAD,PUT,POST,DELETE" 93 | ); 94 | t.deepEqual(res2.header["content-type"], "text/html; charset=utf-8"); 95 | 96 | // test static 97 | const res3: any = await new Promise((resolve, reject) => { 98 | server 99 | .get("/static/test.text") 100 | .end((err, res) => (err ? reject(err) : resolve(res))); 101 | }); 102 | 103 | t.deepEqual(res3.statusCode, 200); 104 | 105 | t.deepEqual(res3.text, "hello static text"); 106 | 107 | // test proxy 108 | const res4: any = await new Promise((resolve, reject) => { 109 | server 110 | .get("/proxy/static/test.text") 111 | .end((err, res) => (err ? reject(err) : resolve(res))); 112 | }); 113 | 114 | t.deepEqual(res4.statusCode, 302); 115 | 116 | t.deepEqual( 117 | res4.header["location"], 118 | "http://www.baidu.com/search/error.html" 119 | ); 120 | }); 121 | 122 | test("app start with custom view build-in feature", async t => { 123 | setCurrentWorkingDir( 124 | path.join(process.cwd(), "build", "__test__", "app-test-example") 125 | ); 126 | 127 | const app = new Application({ 128 | enabled: { 129 | view: {} 130 | } 131 | }); 132 | 133 | await (app).init(); 134 | 135 | const server = request(app.callback()); 136 | 137 | // request view 138 | const res: any = await new Promise((resolve, reject) => { 139 | server.get("/view").end((err, res) => (err ? reject(err) : resolve(res))); 140 | }); 141 | 142 | t.deepEqual( 143 | res.text, 144 | `
145 | hello view 146 |
` 147 | ); 148 | }); 149 | 150 | test("app start with custom cors build-in feature", async t => { 151 | setCurrentWorkingDir( 152 | path.join(process.cwd(), "build", "__test__", "app-test-example") 153 | ); 154 | 155 | const app = new Application({ 156 | enabled: { 157 | cors: { 158 | methods: ["GET"] 159 | } 160 | } 161 | }); 162 | 163 | await (app).init(); 164 | 165 | const server = request(app.callback()); 166 | 167 | // request view 168 | const res: any = await new Promise((resolve, reject) => { 169 | server.get("/").end((err, res) => (err ? reject(err) : resolve(res))); 170 | }); 171 | 172 | t.deepEqual(res.text, "hello world"); 173 | t.deepEqual(res.header["access-control-allow-methods"], "GET"); 174 | }); 175 | 176 | test("app start with custom body parser build-in feature", async t => { 177 | setCurrentWorkingDir( 178 | path.join(process.cwd(), "build", "__test__", "app-test-example") 179 | ); 180 | 181 | const app = new Application({ 182 | enabled: { 183 | bodyParser: {} 184 | } 185 | }); 186 | 187 | await (app).init(); 188 | 189 | const server = request(app.callback()); 190 | 191 | // request view 192 | const res: any = await new Promise((resolve, reject) => { 193 | server.get("/").end((err, res) => (err ? reject(err) : resolve(res))); 194 | }); 195 | 196 | t.deepEqual(res.text, "hello world"); 197 | }); 198 | 199 | test("app start with custom static file build-in feature", async t => { 200 | setCurrentWorkingDir( 201 | path.join(process.cwd(), "build", "__test__", "app-test-example") 202 | ); 203 | 204 | const app = new Application({ 205 | enabled: { 206 | static: { 207 | mount: "/public" 208 | } 209 | } 210 | }); 211 | 212 | await (app).init(); 213 | 214 | const server = request(app.callback()); 215 | 216 | const res: any = await new Promise((resolve, reject) => { 217 | server 218 | .get("/public/test.text") 219 | .end((err, res) => (err ? reject(err) : resolve(res))); 220 | }); 221 | 222 | t.deepEqual(res.statusCode, 200); 223 | 224 | t.deepEqual(res.text, "hello static text"); 225 | }); 226 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import * as Koa from "koa"; 3 | import * as mount from "koa-mount"; 4 | import {Container} from "typedi"; 5 | import {Server} from "http"; 6 | 7 | import {loadController} from "./class/controller"; 8 | import {loadService} from "./class/service"; 9 | import Middleware, { 10 | Middleware$, 11 | resolveMiddleware, 12 | isValidMiddleware 13 | } from "./class/middleware"; 14 | import { 15 | Config$, 16 | BodyParserConfig$, 17 | ViewConfig$, 18 | CorsConfig$, 19 | StaticFileServerConfig$, 20 | loadConfig 21 | } from "./config"; 22 | import Context from "./class/context"; 23 | import {paths} from "./path"; 24 | import {CONTEXT, APP_MIDDLEWARE} from "./const"; 25 | 26 | export interface Application$ { 27 | start(port?: number): Promise; 28 | } 29 | 30 | class Application extends Koa { 31 | constructor(private options: Config$ = {}) { 32 | super(); 33 | this[CONTEXT] = Container.get(Context); 34 | this[APP_MIDDLEWARE] = []; 35 | } 36 | 37 | private async init(): Promise { 38 | // create global context 39 | const context: Context = this[CONTEXT]; 40 | 41 | // load config 42 | const config: any = await loadConfig(); 43 | 44 | // set context; 45 | context.config = config; 46 | context.options = this.options; 47 | 48 | // enabled some feat 49 | if (this.options.enabled) { 50 | const {bodyParser, proxy, view, cors} = this.options.enabled; 51 | const staticServer = this.options.enabled.static; 52 | // enable body parser 53 | if (bodyParser) { 54 | let bodyParserConfig: BodyParserConfig$ = {}; 55 | 56 | // 如果传入一个Object 57 | if (typeof bodyParser === "object") { 58 | bodyParserConfig = bodyParser; 59 | } 60 | super.use(require("koa-bodyparser")(bodyParserConfig)); 61 | } 62 | 63 | // enable static file server 64 | if (staticServer) { 65 | const FileServer = require("koa-static"); 66 | let StaticFileServerConfig: StaticFileServerConfig$ = { 67 | mount: "/static" 68 | }; 69 | 70 | // if pass an object 71 | if (typeof staticServer === "object") { 72 | StaticFileServerConfig = Object.assign( 73 | StaticFileServerConfig, 74 | staticServer 75 | ); 76 | } 77 | 78 | super.use( 79 | mount( 80 | StaticFileServerConfig.mount, 81 | FileServer(paths.static, StaticFileServerConfig) 82 | ) 83 | ); 84 | } 85 | 86 | // enable proxy 87 | if (proxy) { 88 | const proxyServer = require("koa-proxies"); 89 | const options = proxy.options; 90 | 91 | // if not set rewrite 92 | if (!options.rewrite) { 93 | options.rewrite = path => { 94 | return path.replace(new RegExp("^\\" + proxy.mount), ""); 95 | }; 96 | } 97 | 98 | super.use(proxyServer(proxy.mount, proxy.options)); 99 | } 100 | 101 | // enable the view engine 102 | if (view) { 103 | let viewConfig: ViewConfig$ = {}; 104 | if (typeof view === "object") { 105 | viewConfig = view; 106 | } 107 | const views = require("koa-views"); 108 | super.use(views(paths.view, viewConfig)); 109 | } 110 | 111 | // enable cors 112 | if (cors) { 113 | let corsConfig: CorsConfig$ = {}; 114 | if (typeof cors === "object") { 115 | corsConfig = cors; 116 | } 117 | const corsMiddleware = require("koa-cors"); 118 | super.use(corsMiddleware(corsConfig)); 119 | } 120 | } 121 | 122 | // init service 123 | await loadService(); 124 | 125 | // load global middleware 126 | const globalMiddleware = this[APP_MIDDLEWARE]; 127 | globalMiddleware.forEach(element => { 128 | const {middlewareName, options} = element; 129 | const MiddlewareFactory = resolveMiddleware(middlewareName); 130 | 131 | const middleware: Middleware$ = new MiddlewareFactory(); 132 | 133 | if (!isValidMiddleware(middleware)) { 134 | throw new Error(`Invalid middleware "${middlewareName}"`); 135 | } 136 | 137 | // set context and config for middleware 138 | middleware.config = options; 139 | 140 | const koaStyleMiddleware = middleware.pipe.bind(middleware); 141 | 142 | super.use(koaStyleMiddleware); 143 | }); 144 | 145 | // load controller and generate router 146 | const router = await loadController(); 147 | 148 | super.use(router.routes()).use(router.allowedMethods()); 149 | return this; 150 | } 151 | 152 | async start(port?: number): Promise { 153 | await this.init(); 154 | return super.listen(port || process.env.PORT || 3000); 155 | } 156 | 157 | /** 158 | * load middleware 159 | * @param middlewareName middleware name in /project/middlewares/:name or a npm package name 160 | * @param options 161 | */ 162 | use(middlewareName: string | Koa.Middleware, options = {}) { 163 | this[APP_MIDDLEWARE].push({ 164 | middlewareName, 165 | options 166 | }); 167 | return this; 168 | } 169 | } 170 | 171 | export default Application; 172 | -------------------------------------------------------------------------------- /src/class/context.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import Context from "./context"; 3 | import {paths} from "../path"; 4 | 5 | test("every output should be symbol", async t => { 6 | // const context = Container.get(Context); 7 | const context = new Context(); 8 | t.throws(() => { 9 | new Context(); 10 | }); 11 | 12 | t.deepEqual(context.env, process.env); 13 | t.deepEqual(context.config, {}); // default config 14 | t.deepEqual(context.options, {}); //default options 15 | t.deepEqual(context.paths, paths); //default params 16 | }); 17 | -------------------------------------------------------------------------------- /src/class/context.ts: -------------------------------------------------------------------------------- 1 | import {Config$} from "../config"; 2 | import {paths, Path$} from "../path"; 3 | 4 | export interface Context$ { 5 | readonly env: NodeJS.ProcessEnv; 6 | config: any; 7 | options: Config$; 8 | paths: Path$; 9 | } 10 | 11 | let haveInit: boolean = false; 12 | 13 | export default class Context implements Context$ { 14 | config: any = {}; // 加载的配置文件 15 | options: Config$ = {}; // 启动参数 16 | paths: Path$ = paths; // 相关的路径信息 17 | constructor() { 18 | if (haveInit) { 19 | throw new Error( 20 | "The context have been init, please inject instead of new Context()" 21 | ); 22 | } else { 23 | haveInit = true; 24 | } 25 | } 26 | 27 | get env(): NodeJS.ProcessEnv { 28 | return process.env; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/class/controller.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | 3 | import * as path from "path"; 4 | import {Get} from "../decorators/http"; 5 | import Controller, {ControllerFactory$, loadController} from "./controller"; 6 | import {ROUTER} from "../const"; 7 | import {setCurrentWorkingDir, paths} from "../path"; 8 | import * as request from "supertest"; 9 | import * as Koa from "koa"; 10 | 11 | const originCwd = process.cwd(); 12 | 13 | test("http decorator", async t => { 14 | let Factory: ControllerFactory$; 15 | t.notThrows(function () { 16 | class HomeController extends Controller { 17 | @Get("/index") 18 | async index(ctx, next) { 19 | ctx.body = "hello kost"; 20 | } 21 | } 22 | 23 | Factory = HomeController; 24 | }); 25 | 26 | const ctrl = new Factory(); 27 | 28 | t.deepEqual(ctrl[ROUTER].length, 1); 29 | t.deepEqual(ctrl[ROUTER], [ 30 | { 31 | path: "/index", 32 | method: "get", 33 | handler: "index" 34 | } 35 | ]); 36 | }); 37 | 38 | test.serial("load controller", async t => { 39 | setCurrentWorkingDir( 40 | path.join(process.cwd(), "build", "__test__", "controller-test-example") 41 | ); 42 | const router = await loadController(); 43 | 44 | const stack = router.stack; 45 | t.deepEqual(stack[0].path, "/"); 46 | t.deepEqual(stack[1].path, "/name"); 47 | 48 | const app = new Koa(); 49 | 50 | app.use(router.routes()).use(router.allowedMethods()); 51 | 52 | const server = request(app.callback()); 53 | 54 | const res1: any = await new Promise(function (resolve, reject) { 55 | server.get("/").end((err, res) => (err ? reject(err) : resolve(res))); 56 | }); 57 | 58 | t.deepEqual(res1.text, "hello world"); 59 | 60 | const res2: any = await new Promise(function (resolve, reject) { 61 | server.get("/name").end((err, res) => (err ? reject(err) : resolve(res))); 62 | }); 63 | 64 | t.deepEqual(res2.text, "axetroy"); 65 | }); 66 | 67 | test.serial("load controller with invalid middleware", async t => { 68 | setCurrentWorkingDir( 69 | path.join( 70 | process.cwd(), 71 | "build", 72 | "__test__", 73 | "controller-with-invalid-middleware-test-example" 74 | ) 75 | ); 76 | 77 | try { 78 | await loadController(); 79 | t.fail("Load controller should be fail, cause middleware is invalid"); 80 | } catch (err) { 81 | t.true(err instanceof Error); 82 | } 83 | }); 84 | 85 | test.serial("load invalid controller", async t => { 86 | setCurrentWorkingDir( 87 | path.join( 88 | process.cwd(), 89 | "build", 90 | "__test__", 91 | "invalid-controller-test-example" 92 | ) 93 | ); 94 | 95 | try { 96 | await loadController(); 97 | t.fail("Load controller should be fail, cause controller is invalid"); 98 | } catch (err) { 99 | t.true(err instanceof Error); 100 | } 101 | }); 102 | -------------------------------------------------------------------------------- /src/class/controller.ts: -------------------------------------------------------------------------------- 1 | import {MiddlewareFactory$} from "./middleware"; 2 | import {ROUTER, MIDDLEWARE} from "../const"; 3 | import {readdir} from "fs-extra"; 4 | import * as path from "path"; 5 | import {paths} from "../path"; 6 | import {Container} from "typedi"; 7 | import * as Router from "koa-router"; 8 | import {isValidMiddleware} from "./middleware"; 9 | import {getOutput} from "../utils"; 10 | 11 | export interface Router$ { 12 | method: string; 13 | path: string | RegExp; 14 | handler: string; 15 | } 16 | 17 | export interface ControllerMiddleware$ { 18 | handler: string; 19 | factory: MiddlewareFactory$; 20 | options?: any; 21 | } 22 | 23 | export interface ControllerFactory$ { 24 | new (): Controller$; 25 | } 26 | 27 | export interface Controller$ { 28 | } 29 | 30 | export default class Controller implements Controller$ { 31 | constructor() { 32 | this[ROUTER] = this[ROUTER] || []; 33 | this[MIDDLEWARE] = this[MIDDLEWARE] || []; 34 | } 35 | } 36 | 37 | /** 38 | * check the object is a valid controller 39 | * @param c 40 | */ 41 | export function isValidController(c: any): boolean { 42 | return c instanceof Controller; 43 | } 44 | 45 | export async function loadController(): Promise { 46 | const controllerFiles = (await readdir(paths.controller)).filter(file => 47 | /\.controller\.t|jsx?$/.test(file) 48 | ); 49 | const controllers: Controller$[] = []; 50 | 51 | const env: string = process.env.NODE_ENV; 52 | 53 | // load controller 54 | while (controllerFiles.length) { 55 | const controllerFile = controllerFiles.shift(); 56 | const filePath: string = path.join(paths.controller, controllerFile); 57 | const YourController = getOutput(require(filePath)); 58 | 59 | const ctrl: Controller$ = Container.get(YourController); 60 | 61 | if (!isValidController(ctrl)) { 62 | throw new Error(`The file ${filePath} is not a controller file.`); 63 | } 64 | 65 | controllers.push(ctrl); 66 | } 67 | 68 | const router = new Router(); 69 | 70 | // resolve controller 71 | for (let controller of controllers) { 72 | const routers: Router$[] = controller[ROUTER]; 73 | for (let i = 0; i < routers.length; i++) { 74 | const route: Router$ = routers[i]; 75 | const handler = controller[route.handler]; 76 | 77 | // get the middleware for this route 78 | const controllerMiddleware = controller[MIDDLEWARE].filter( 79 | (m: ControllerMiddleware$) => m.handler === route.handler 80 | ).map((m: ControllerMiddleware$) => { 81 | const middleware = new m.factory(); 82 | middleware.config = m.options; 83 | return middleware; 84 | }); 85 | 86 | if (env !== "production") { 87 | console.log(`[${route.method.toUpperCase()}] ${route.path}`); 88 | } 89 | 90 | controllerMiddleware.forEach(m => { 91 | if (!isValidMiddleware(m)) { 92 | throw new Error(`Invalid middleware in controller .${route.handler}`); 93 | } 94 | }); 95 | 96 | router[route.method]( 97 | route.path, 98 | ...controllerMiddleware.map(m => m.pipe.bind(m)), // middleware 99 | async (ctx, next) => handler.call(controller, ctx, next) 100 | ); 101 | } 102 | } 103 | 104 | return router; 105 | } 106 | -------------------------------------------------------------------------------- /src/class/middleware.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import * as path from "path"; 3 | import Middleware, { 4 | resolveMiddleware, 5 | MiddlewareFactory$, 6 | isValidMiddleware 7 | } from "./middleware"; 8 | import Kost from "../app"; 9 | import * as request from "supertest"; 10 | 11 | import {setCurrentWorkingDir} from "../path"; 12 | 13 | test("service", async t => { 14 | let MiddlewareFactory: MiddlewareFactory$; 15 | 16 | class MyMiddleware extends Middleware { 17 | async pipe(ctx, next) { 18 | } 19 | } 20 | 21 | const middleware = new MyMiddleware(); 22 | // default 23 | t.deepEqual(middleware.config, {}); 24 | }); 25 | 26 | test.serial("resolve valid Middleware", async t => { 27 | const cwd = path.join(process.cwd(), "build", "__test__", "middleware-test-example"); 28 | setCurrentWorkingDir(cwd); 29 | t.notThrows(() => { 30 | const LoggerMiddleware = resolveMiddleware("logger"); 31 | }); 32 | t.throws(function () { 33 | // invalid middleware, it should throw an error 34 | resolveMiddleware("aabbcc"); 35 | }); 36 | 37 | // setup server 38 | const app = new Kost().use("logger"); 39 | await (app).init(); 40 | 41 | const server = request(app.callback()); 42 | 43 | await new Promise((resolve, reject) => { 44 | server.get("/").end((err, res) => (err ? reject(err) : resolve(res))); 45 | }); 46 | 47 | t.pass(); 48 | }); 49 | 50 | test.serial("resolve invalid Middleware", async t => { 51 | const cwd = path.join( 52 | process.cwd(), 53 | "build", 54 | "__test__", 55 | "invalid-middleware-test-example" 56 | ); 57 | setCurrentWorkingDir(cwd); 58 | t.notThrows(() => { 59 | const LoggerMiddleware = resolveMiddleware("limit"); 60 | }); 61 | 62 | // setup server 63 | const app = new Kost().use("limit"); 64 | 65 | try { 66 | await (app).init(); 67 | t.fail("Middleware is invalid, it should be fail."); 68 | } catch (err) { 69 | t.true(err instanceof Error); 70 | } 71 | }); 72 | 73 | test.serial("compatible with koa middleware", async t => { 74 | // setup server 75 | const app = new Kost().use(function (ctx, next) { 76 | ctx.body = "hello koa middleware"; 77 | }); 78 | 79 | // it should be init success 80 | await (app).init(); 81 | 82 | const server = request(app.callback()); 83 | 84 | const res: any = await new Promise((resolve, reject) => { 85 | server.get("/").end((err, res) => (err ? reject(err) : resolve(res))); 86 | }); 87 | 88 | t.deepEqual(res.text, "hello koa middleware"); 89 | }); 90 | 91 | test("isValidMiddleware", async t => { 92 | t.false(isValidMiddleware(new Middleware())); 93 | 94 | class MyMiddleware extends Middleware { 95 | async pipe(ctx, next) { 96 | next(); 97 | } 98 | } 99 | 100 | t.true(isValidMiddleware(new MyMiddleware())); 101 | 102 | class MiddlewareWithoutPipe extends Middleware { 103 | } 104 | 105 | t.false(isValidMiddleware(new MiddlewareWithoutPipe())); 106 | }); 107 | -------------------------------------------------------------------------------- /src/class/middleware.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as Koa from "koa"; 3 | import {paths} from "../path"; 4 | import {getOutput} from "../utils"; 5 | 6 | export interface Middleware$ { 7 | config: any; 8 | pipe: Koa.Middleware; 9 | } 10 | 11 | export interface MiddlewareFactory$ { 12 | new (): Middleware$; 13 | } 14 | 15 | export default class Middleware implements Middleware$ { 16 | public config: any = {}; 17 | 18 | constructor(options?: any) { 19 | this.config = options || {}; 20 | } 21 | 22 | /** 23 | * the pipe function, same with koa middleware 24 | * @param ctx 25 | * @param next 26 | */ 27 | async pipe(ctx, next): Promise { 28 | 29 | } 30 | } 31 | 32 | /** 33 | * 通过中间件的字符串,获取中间件类 34 | * @param middlewareName 35 | * @param cwd 36 | */ 37 | export function resolveMiddleware(middlewareName: string | Koa.Middleware): MiddlewareFactory$ { 38 | if (typeof middlewareName === "function") { 39 | return class KoaMiddleware extends Middleware { 40 | async pipe(ctx, next) { 41 | return middlewareName(ctx, next); 42 | } 43 | }; 44 | } 45 | 46 | let MiddlewareFactory: MiddlewareFactory$; 47 | try { 48 | const localMiddlewarePath = path.join( 49 | paths.middleware, 50 | middlewareName + ".middleware" 51 | ); 52 | // require from local 53 | MiddlewareFactory = getOutput(require(localMiddlewarePath)); 54 | } catch (err) { 55 | // if not found in local middleware dir 56 | // require from node_modules 57 | try { 58 | MiddlewareFactory = getOutput(require(middlewareName)); 59 | } catch (err) { 60 | throw new Error(`Can not found the middleware "${middlewareName}"`); 61 | } 62 | } 63 | 64 | return MiddlewareFactory; 65 | } 66 | 67 | const defaultMiddleware = new Middleware(); 68 | 69 | /** 70 | * check the object is a valid middleware or not 71 | * @param m 72 | */ 73 | export function isValidMiddleware(m: any): boolean { 74 | return m instanceof Middleware && m.pipe !== defaultMiddleware.pipe; 75 | } 76 | -------------------------------------------------------------------------------- /src/class/service.test.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import test from "ava"; 3 | import {Inject, Container} from "typedi"; 4 | import * as path from "path"; 5 | import Service, {ServiceFactory$, loadService} from "./service"; 6 | import {setCurrentWorkingDir} from "../path"; 7 | import UserService from "../../__test__/service-test-example/services/user.service.js"; 8 | import LogService from "../../__test__/service-test-example/services/log.service.js"; 9 | 10 | test("service", async t => { 11 | let serviceFactory: ServiceFactory$; 12 | t.notThrows(function () { 13 | class MyService extends Service { 14 | enable = true; 15 | level = 0; 16 | 17 | async init() { 18 | } 19 | } 20 | 21 | serviceFactory = MyService; 22 | }); 23 | const service = Container.get(serviceFactory); 24 | 25 | // default property 26 | t.deepEqual(service.level, 0); 27 | t.deepEqual(service.enable, true); 28 | }); 29 | 30 | test("service inject service", async t => { 31 | class A extends Service { 32 | } 33 | 34 | class B extends Service { 35 | @Inject() a: A; 36 | } 37 | 38 | const service = Container.get(B); 39 | 40 | t.true(service.a instanceof A); 41 | t.true(service instanceof B); 42 | }); 43 | 44 | test("Inject service", async t => { 45 | class A extends Service { 46 | } 47 | 48 | class B extends Service { 49 | @Inject() a: A; 50 | } 51 | 52 | const service = Container.get(B); 53 | 54 | t.true(service.a instanceof A); 55 | t.true(service instanceof B); 56 | }); 57 | 58 | test.serial("Load service", async t => { 59 | setCurrentWorkingDir( 60 | path.join(process.cwd(), "build", "__test__", "service-test-example") 61 | ); 62 | 63 | await loadService(); 64 | 65 | // service have been init here 66 | 67 | const user = Container.get(UserService); 68 | const logger = Container.get(LogService); 69 | 70 | t.deepEqual(user.username, "admin"); 71 | 72 | t.deepEqual(await user.getUser(), {name: "Axetroy"}); 73 | 74 | // logger service should be inited before user service 75 | t.true(logger.initedAt.getTime() < user.initedAt.getTime()); 76 | }); 77 | 78 | test.serial("Load invalid service", async t => { 79 | setCurrentWorkingDir( 80 | path.join( 81 | process.cwd(), 82 | "build", 83 | "__test__", 84 | "invalid-service-test-example" 85 | ) 86 | ); 87 | 88 | try { 89 | await loadService(); 90 | t.fail("It should throw an error"); 91 | } catch (err) { 92 | t.true(err instanceof Error); 93 | } 94 | }); 95 | -------------------------------------------------------------------------------- /src/class/service.ts: -------------------------------------------------------------------------------- 1 | import {Application$} from "../app"; 2 | import {readdir, pathExists} from "fs-extra"; 3 | import * as path from "path"; 4 | import {paths} from "../path"; 5 | import {Container} from "typedi"; 6 | import {getOutput} from "../utils"; 7 | 8 | export interface Service$ { 9 | enable: boolean; 10 | level: number; 11 | 12 | init(app: Application$): Promise; 13 | } 14 | 15 | export interface ServiceFactory$ { 16 | new (): Service$; 17 | } 18 | 19 | export default class Service implements Service$ { 20 | public level: number = 0; // the level of service 21 | public enable = true; // default true 22 | constructor() { 23 | } 24 | 25 | async init(): Promise { 26 | } 27 | } 28 | 29 | /** 30 | * check is a valid service or not 31 | * @param s 32 | */ 33 | export function isValidService(s: any): boolean { 34 | return s instanceof Service; 35 | } 36 | 37 | /** 38 | * load service 39 | */ 40 | export async function loadService(): Promise { 41 | const serviceFiles: string[] = ((await pathExists(paths.service)) 42 | ? await readdir(paths.service) 43 | : [] 44 | ).filter(file => /\.service.t|jsx?$/.test(file)); 45 | 46 | // init service 47 | const services: Service$[] = serviceFiles 48 | .map(serviceFile => { 49 | const filePath: string = path.join(paths.service, serviceFile); 50 | const ServiceFactory = getOutput(require(filePath)); 51 | const service = Container.get(ServiceFactory); 52 | if (!isValidService(service)) { 53 | throw new Error(`The file ${filePath} is not a service file.`); 54 | } 55 | return service; 56 | }) 57 | .sort((a: Service$) => -a.level); // sort by service's level 58 | 59 | while (services.length) { 60 | const service = services.shift(); 61 | await service.init(this); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/config.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import * as path from "path"; 3 | import {loadConfig} from "./config"; 4 | import {setCurrentWorkingDir} from "./path"; 5 | 6 | test.serial("load none config", async t => { 7 | const cwd = path.join(process.cwd(), "__test__", "config-none-test-example"); 8 | setCurrentWorkingDir(cwd); 9 | 10 | const config = await loadConfig(); 11 | 12 | t.true(typeof config === "object"); 13 | t.deepEqual(Object.keys(config).length, 0); 14 | }); 15 | 16 | test.serial("load default config", async t => { 17 | const cwd = path.join( 18 | process.cwd(), 19 | "__test__", 20 | "config-default-test-example" 21 | ); 22 | setCurrentWorkingDir(cwd); 23 | 24 | const config = await loadConfig(); 25 | 26 | t.true(typeof config === "object"); 27 | t.deepEqual(config.name, "axetroy"); 28 | t.deepEqual(Object.keys(config).length, 1); 29 | }); 30 | 31 | test.serial("load default config and env config", async t => { 32 | const cwd = path.join(process.cwd(), "__test__", "config-all-test-example"); 33 | setCurrentWorkingDir(cwd); 34 | 35 | const config = await loadConfig(); 36 | 37 | t.true(typeof config === "object"); 38 | t.deepEqual(config.env, "test"); 39 | t.deepEqual(Object.keys(config).length, 2); 40 | }); 41 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import * as proxyServer from "http-proxy"; 2 | import * as Koa from "koa"; 3 | import * as yaml from "js-yaml"; 4 | import * as fs from "fs-extra"; 5 | import * as path from "path"; 6 | import {paths} from "./path"; 7 | 8 | export interface ProxyConfig$ extends proxyServer.ServerOptions { 9 | rewrite?(path: string): string; 10 | 11 | cookieDomainRewrite?: boolean; 12 | logs?: boolean; 13 | } 14 | 15 | export interface BodyParserConfig$ { 16 | enableTypes?: string[]; 17 | encode?: string; 18 | formLimit?: string; 19 | jsonLimit?: string; 20 | strict?: boolean; 21 | detectJSON?: (ctx: Koa.Context) => boolean; 22 | extendTypes?: { 23 | json?: string[]; 24 | form?: string[]; 25 | text?: string[]; 26 | }; 27 | onerror?: (err: Error, ctx: Koa.Context) => void; 28 | } 29 | 30 | export interface ViewConfig$ { 31 | /* 32 | * default extension for your views 33 | */ 34 | extension?: string; 35 | /* 36 | * these options will get passed to the view engine 37 | */ 38 | options?: any; 39 | /* 40 | * map a file extension to an engine 41 | */ 42 | map?: any; 43 | /* 44 | * replace consolidate as default engine source 45 | */ 46 | engineSource?: any; 47 | } 48 | 49 | export type originHandler = (ctx: Koa.Request) => string; 50 | 51 | export interface CorsConfig$ { 52 | origin?: string | boolean | originHandler; 53 | expose?: string[]; 54 | maxAge?: number; 55 | credentials?: boolean; 56 | methods?: string[]; 57 | headers?: string[]; 58 | } 59 | 60 | export interface StaticFileServerConfig$ { 61 | mount: string; 62 | maxage?: number; 63 | hidden?: boolean; 64 | index?: string; 65 | defer?: boolean; 66 | gzip?: boolean; 67 | br?: boolean; 68 | 69 | setHeaders?(res: any, path: string, stats: any): any; 70 | 71 | extensions?: boolean; 72 | } 73 | 74 | export interface Config$ { 75 | enabled?: { 76 | static?: boolean | StaticFileServerConfig$; 77 | proxy?: { 78 | mount: string; 79 | options: ProxyConfig$; 80 | }; 81 | cors?: boolean | CorsConfig$; 82 | bodyParser?: boolean | BodyParserConfig$; 83 | view?: boolean | ViewConfig$; 84 | }; 85 | } 86 | 87 | /** 88 | * load config 89 | */ 90 | export async function loadConfig(): Promise { 91 | const defaultConfigPath = path.join(paths.config, "default.config.yaml"); 92 | const envConfigPath = path.join( 93 | paths.config, 94 | process.env.NODE_ENV + ".config.yaml" 95 | ); 96 | // load default config if it exist 97 | const defaultConfig = (await fs.pathExists(defaultConfigPath)) 98 | ? yaml.safeLoad(await fs.readFile(defaultConfigPath, "utf8")) 99 | : {}; 100 | 101 | // load env config if it exist 102 | const envConfig = (await fs.pathExists(envConfigPath)) 103 | ? yaml.safeLoad(fs.readFileSync(envConfigPath, "utf8")) 104 | : {}; 105 | 106 | const config: any = Object.assign({}, defaultConfig, envConfig); 107 | 108 | return config; 109 | } 110 | -------------------------------------------------------------------------------- /src/const.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import * as constant from "./const"; 3 | 4 | test("every output should be symbol", async t => { 5 | for (let attr in constant) { 6 | let val = constant[attr]; 7 | t.deepEqual(typeof val, "symbol"); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /src/const.ts: -------------------------------------------------------------------------------- 1 | const ROUTER = Symbol("controller#router"); 2 | const MIDDLEWARE = Symbol("controller#middleware"); 3 | const CONTEXT = Symbol("app#context"); 4 | const APP_MIDDLEWARE = Symbol("app#middleware"); 5 | 6 | export {ROUTER, MIDDLEWARE, CONTEXT, APP_MIDDLEWARE}; 7 | -------------------------------------------------------------------------------- /src/decorators/http.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import {Get, Post, Put, Delete, Head, Patch, Options, All} from "./http"; 3 | import Controller, {ControllerFactory$} from "../class/controller"; 4 | import {ROUTER} from "../const"; 5 | 6 | test("http decorator", async t => { 7 | let Factory: ControllerFactory$; 8 | t.notThrows(function () { 9 | class HomeController extends Controller { 10 | @Get("/index") 11 | @Post("/index") 12 | @Put("/index") 13 | @Head("/index") 14 | @Patch("/index") 15 | @All("/index") 16 | @Options("/index") 17 | async index(ctx, next) { 18 | ctx.body = "hello kost"; 19 | } 20 | 21 | @Delete("/a") 22 | async del() { 23 | } 24 | } 25 | 26 | Factory = HomeController; 27 | }); 28 | 29 | const ctrl = new Factory(); 30 | 31 | t.deepEqual(ctrl[ROUTER].length, 8); 32 | }); 33 | 34 | test("http decorator in customer controller should throw an error", async t => { 35 | let Factory: ControllerFactory$; 36 | t.throws(function () { 37 | class HomeController { 38 | @Post("/index") 39 | async index(ctx, next) { 40 | ctx.body = "hello kost"; 41 | } 42 | } 43 | 44 | Factory = HomeController; 45 | }); 46 | }); 47 | 48 | test("http decorator in not function", async t => { 49 | let Factory: ControllerFactory$; 50 | t.throws(function () { 51 | class HomeController extends Controller { 52 | // @ts-ignore: 53 | @Post("/index") user: any; 54 | } 55 | 56 | Factory = HomeController; 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/decorators/http.ts: -------------------------------------------------------------------------------- 1 | import Controller, {Controller$, Router$} from "../class/controller"; 2 | import {ROUTER} from "../const"; 3 | 4 | /** 5 | * decorator for controller 6 | * @param method 7 | * @param path 8 | */ 9 | function request(method: string, path: string | RegExp) { 10 | return function (target: Controller$, 11 | propertyKey: string, 12 | descriptor: PropertyDescriptor) { 13 | if ( 14 | target instanceof Controller === false || 15 | typeof target[propertyKey] !== "function" 16 | ) { 17 | throw new Error(`@${method} decorator is only for class method`); 18 | } 19 | const routers: Router$[] = target[ROUTER] || []; 20 | routers.push({ 21 | path: path, 22 | method: method.toLocaleLowerCase(), 23 | handler: propertyKey 24 | }); 25 | target[ROUTER] = routers; 26 | }; 27 | } 28 | 29 | export function Get(path: string | RegExp) { 30 | return request("GET", path); 31 | } 32 | 33 | export function Post(path: string | RegExp) { 34 | return request("POST", path); 35 | } 36 | 37 | export function Put(path: string | RegExp) { 38 | return request("PUT", path); 39 | } 40 | 41 | export function Delete(path: string | RegExp) { 42 | return request("DELETE", path); 43 | } 44 | 45 | export function Head(path: string | RegExp) { 46 | return request("HEAD", path); 47 | } 48 | 49 | export function Patch(path: string | RegExp) { 50 | return request("PATCH", path); 51 | } 52 | 53 | export function Options(path: string | RegExp) { 54 | return request("OPTIONS", path); 55 | } 56 | 57 | export function All(path: string | RegExp) { 58 | return request("ALL", path); 59 | } 60 | -------------------------------------------------------------------------------- /src/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./http"; 2 | export * from "./middleware"; 3 | -------------------------------------------------------------------------------- /src/decorators/middleware.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | 3 | import * as path from "path"; 4 | import {Use} from "./middleware"; 5 | import {resolveMiddleware} from "../class/middleware"; 6 | import Controller, {ControllerFactory$} from "../class/controller"; 7 | import {MIDDLEWARE} from "../const"; 8 | import {setCurrentWorkingDir} from "../path"; 9 | 10 | test("middleware decorator is only use in controller", async t => { 11 | setCurrentWorkingDir( 12 | path.join(process.cwd(), "build", "__test__", "middleware-test-example") 13 | ); 14 | 15 | const LoggerMiddleware = resolveMiddleware("logger"); 16 | 17 | let Factory: ControllerFactory$; 18 | t.notThrows(function () { 19 | class HomeController extends Controller { 20 | @Use("logger") 21 | async index(ctx, next) { 22 | ctx.body = "hello kost"; 23 | } 24 | } 25 | 26 | Factory = HomeController; 27 | }); 28 | 29 | const ctrl = new Factory(); 30 | t.deepEqual(ctrl[MIDDLEWARE].length, 1); 31 | t.deepEqual(ctrl[MIDDLEWARE], [ 32 | { 33 | handler: "index", 34 | factory: LoggerMiddleware, 35 | options: {} 36 | } 37 | ]); 38 | 39 | // if the decorator use in a customer class 40 | t.throws(function () { 41 | class Abc { 42 | router = []; 43 | middleware = []; 44 | 45 | @Use("logger") 46 | async index(ctx, next) { 47 | } 48 | } 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/decorators/middleware.ts: -------------------------------------------------------------------------------- 1 | import {resolveMiddleware} from "../class/middleware"; 2 | import Controller, {Controller$, ControllerMiddleware$} from "../class/controller"; 3 | import {MIDDLEWARE} from "../const"; 4 | 5 | /** 6 | * decorator of controller to inject the middleware 7 | * @param middlewareName 8 | * @param options 9 | */ 10 | export function Use(middlewareName: string, options: any = {}) { 11 | const MiddlewareFactory = resolveMiddleware(middlewareName); 12 | return function (target: Controller$, 13 | propertyKey: string, 14 | descriptor: PropertyDescriptor) { 15 | if ( 16 | target instanceof Controller === false || 17 | typeof target[propertyKey] !== "function" 18 | ) { 19 | throw new Error("@USE() decorator only for controller method"); 20 | } 21 | const controllerMiddleware: ControllerMiddleware$[] = target[MIDDLEWARE] || []; 22 | controllerMiddleware.push({ 23 | handler: propertyKey, 24 | factory: MiddlewareFactory, 25 | options 26 | }); 27 | target[MIDDLEWARE] = controllerMiddleware; 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/path.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import {paths} from "./path"; 3 | 4 | test("paths", async t => { 5 | t.deepEqual(paths.cwd, process.cwd()); 6 | t.true(typeof paths.config === "string"); 7 | t.true(typeof paths.controller === "string"); 8 | t.true(typeof paths.middleware === "string"); 9 | t.true(typeof paths.service === "string"); 10 | t.true(typeof paths.static === "string"); 11 | t.true(typeof paths.view === "string"); 12 | }); 13 | -------------------------------------------------------------------------------- /src/path.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | 3 | export interface Path$ { 4 | cwd: string; 5 | controller: string; 6 | config: string; 7 | middleware: string; 8 | service: string; 9 | static: string; 10 | view: string; 11 | } 12 | 13 | let paths: Path$; 14 | 15 | /** 16 | * set current working dir 17 | * @param cwd 18 | */ 19 | export function setCurrentWorkingDir(cwd: string): void { 20 | paths = paths || {}; 21 | paths.cwd = cwd; 22 | paths.config = path.join(cwd, "configs"); 23 | paths.controller = path.join(cwd, "controllers"); 24 | paths.middleware = path.join(cwd, "middlewares"); 25 | paths.service = path.join(cwd, "services"); 26 | paths.static = path.join(cwd, "static"); 27 | paths.view = path.join(cwd, "views"); 28 | } 29 | 30 | setCurrentWorkingDir(process.cwd()); 31 | 32 | export {paths}; 33 | -------------------------------------------------------------------------------- /src/utils.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import {getOutput} from "./utils"; 3 | 4 | test("get output", t => { 5 | t.deepEqual( 6 | getOutput({ 7 | default: 123 8 | }), 9 | 123 10 | ); 11 | 12 | t.deepEqual(getOutput([]), []); 13 | t.deepEqual(getOutput(undefined), undefined); 14 | }); 15 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function getOutput(output) { 2 | return output && output.default ? output.default : output; 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "ES2017", 5 | /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ 6 | "module": "commonjs", 7 | /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 8 | "lib": ["dom", "es2015", "es2016", "es2017", "esnext"], 9 | /* Specify library files to be included in the compilation: */ 10 | "allowJs": false /* Allow javascript files to be compiled. */, 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | "sourceMap": true /* Generates corresponding '.map' file. */, 15 | // "outFile": "./", /* Concatenate and emit output to single file. */ 16 | "outDir": "./build" /* Redirect output structure to the directory. */, 17 | // "rootDir": 18 | // "example" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 19 | "removeComments": true /* Do not emit comments to output. */, 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | /* Strict Type-Checking Options */ 25 | "strict": true, 26 | /* Enable all strict type-checking options. */ 27 | "noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */, 28 | "strictNullChecks": false /* Enable strict null checks. */, 29 | "noImplicitThis": false /* Raise error on 'this' expressions with an implied 'any' type. */, 30 | "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 31 | 32 | /* Additional Checks */ 33 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 34 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 35 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 36 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 37 | 38 | /* Module Resolution Options */ 39 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 40 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 41 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 42 | "rootDirs": [ 43 | "example", 44 | "src", 45 | "__test__", 46 | "./" 47 | ] /* List of root folders whose combined content represents the structure of the project at runtime. */, 48 | "typeRoots": ["node_modules/@types"], 49 | /* List of folders to include type definitions from. */ 50 | "types": ["node"], 51 | /* Type declaration files to be included in compilation. */ 52 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 53 | 54 | /* Source Map Options */ 55 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 56 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 57 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 58 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 59 | 60 | /* Experimental Options */ 61 | "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, 62 | "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */ 63 | }, 64 | "include": ["src/**/*", "example/**/*", "test", "__test__", "./"], 65 | "exclude": ["node_modules"] 66 | } 67 | --------------------------------------------------------------------------------