├── . 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 | [](https://travis-ci.org/axetroy/kost)
4 | [](https://coveralls.io/github/axetroy/kost?branch=master)
5 | [](https://david-dm.org/axetroy/kost)
6 | 
7 | [](https://github.com/prettier/prettier)
8 | 
9 | [](https://badge.fury.io/js/%40axetroy%2Fkost)
10 | 
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 | 
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 |
--------------------------------------------------------------------------------