├── .github └── FUNDING.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── alias.js ├── babel.js ├── check.js ├── config.js ├── controller.js ├── core ├── config.js ├── controller.js ├── plugin.js ├── responder.js ├── router.js └── service.js ├── docs ├── API.md ├── README.md ├── configuration.md ├── controller.md ├── guide.md ├── installation.md ├── plugin.md ├── router.md └── service.md ├── example ├── config │ └── default.js ├── index.js ├── public │ └── index.html └── server │ ├── controllers │ └── home.js │ └── templates │ └── home.jade ├── import.js ├── index.js ├── model.js ├── package.json ├── plugins ├── cookie.js ├── database.js ├── defaultHandler.js ├── errorHandler.js ├── logger.js ├── mongodb.js ├── oss.js ├── render.js ├── sentry.js └── static.js ├── react.js ├── render.js ├── renders ├── default.js ├── ejs.js ├── handlebars.js ├── jade.js └── nunjucks.js ├── response.js ├── router.js ├── services ├── accepts.js ├── body.js ├── cookies.js ├── headers.js ├── ip.js ├── params.js └── query.js ├── start.js ├── test └── index.js └── worker.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: song940 4 | patreon: song940 5 | open_collective: song940 6 | ko_fi: song940 7 | tidelift: npm/kanary 8 | custom: https://git.io/fjRcB 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.log 3 | yarn.lock 4 | 5 | node_modules/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | *.log 3 | yarn.lock 4 | 5 | node_modules/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 lsong 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Kanary 2 | 3 | > The next full-featured javascript frameworks. 4 | 5 | [![kanary](https://img.shields.io/npm/v/kanary.svg)](https://npmjs.org/kanary) 6 | 7 | ### Installation 8 | 9 | ```bash 10 | $ npm install kanary 11 | ``` 12 | 13 | ### Example 14 | 15 | ```js 16 | require('kanary/start'); 17 | ``` 18 | 19 | ### Documents 20 | 21 | [docs](./docs) 22 | 23 | ### Benchmarks 24 | 25 | MacBook Pro (Retina, 15-inch, Mid 2015) 2.2 GHz Intel Core i7 / 16 GB 1600 MHz DDR3 26 | 27 | | Framework | Version | Router | Requests\/s | 28 | | :--- | --: | :-: | --: | 29 | | http | 10.13.0 | ✗ | 32534.93 | 30 | | express | 4.16.4 | ✓ | 13088.20 | 31 | | koa | 2.6.2 | ✗ | 22540.10 | 32 | | kelp | 2.0.1 | ✗ | 30707.83 | 33 | | kanary | 2.0.0 | ✓ | 22689.01 | 34 | | fastify | 1.13.0 | ✓ | 31248.66 | 35 | 36 | https://github.com/song940/kanary-benchmarks 37 | 38 | ### Contributing 39 | - Fork this Repo first 40 | - Clone your Repo 41 | - Install dependencies by `$ npm install` 42 | - Checkout a feature branch 43 | - Feel free to add your features 44 | - Make sure your features are fully tested 45 | - Publish your local branch, Open a pull request 46 | - Enjoy hacking <3 47 | 48 | ### MIT 49 | 50 | This work is licensed under the [MIT license](./LICENSE). 51 | 52 | --- 53 | -------------------------------------------------------------------------------- /alias.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const path = require('path'); 3 | const { Module } = require('module'); 4 | const config = require('./config'); 5 | 6 | const findPath = Module._findPath; 7 | Module._findPath = (request, paths, isMain) => { 8 | for (const key in config.path) { 9 | const prefix = `@${key}/`; 10 | if (request.startsWith(prefix)) { 11 | request = request.replace(prefix, ''); 12 | request = path.join(config.path[key], request); 13 | break; 14 | } 15 | } 16 | return findPath(request, paths, isMain); 17 | }; -------------------------------------------------------------------------------- /babel.js: -------------------------------------------------------------------------------- 1 | require('@babel/register')({ 2 | babelrc: false, 3 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 4 | presets: [ 5 | '@babel/preset-react', 6 | '@babel/preset-typescript' 7 | ], 8 | plugins: [ 9 | ['@babel/plugin-transform-modules-commonjs'], 10 | ["@babel/plugin-proposal-decorators", { legacy: true }], 11 | ] 12 | }); -------------------------------------------------------------------------------- /check.js: -------------------------------------------------------------------------------- 1 | // npm engines seems to be broken since npm v3.6.0. 2 | // https://github.com/npm/npm/issues/12486 3 | const semver = require('semver'); 4 | const { name, engines = {} } = require('./package'); 5 | // https://docs.npmjs.com/files/package.json#engines 6 | Object.keys(engines).forEach(engine => { 7 | const current = process.versions[engine]; 8 | const range = engines[engine]; 9 | if (!semver.satisfies(current, range)) { 10 | console.error(`${name} requires ${engine} version ${range}, but current is ${current}`); 11 | process.exit(1); 12 | } 13 | }); -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const config = require('kelp-config/config'); 3 | 4 | const root = process.cwd(); 5 | const $ = p => join(root, p); 6 | 7 | const defaults = { 8 | port: 3000, 9 | path: { 10 | root, 11 | public: $('public'), 12 | client: $('client'), 13 | server: $('server'), 14 | models: $('server/models'), 15 | plugins: $('server/plugins'), 16 | services: $('server/services'), 17 | templates: $('server/templates'), 18 | controllers: $('server/controllers'), 19 | }, 20 | routes: [], 21 | plugins: [], 22 | }; 23 | 24 | module.exports = config(defaults); 25 | -------------------------------------------------------------------------------- /controller.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | 3 | class Controller extends EventEmitter { 4 | 5 | } 6 | 7 | module.exports = Controller; -------------------------------------------------------------------------------- /core/config.js: -------------------------------------------------------------------------------- 1 | const config = require('../config'); 2 | 3 | module.exports = app => { 4 | app.config = config; 5 | return (req, res, next) => next(); 6 | }; -------------------------------------------------------------------------------- /core/controller.js: -------------------------------------------------------------------------------- 1 | const glob = require('glob'); 2 | const Controller = require('../controller'); 3 | 4 | const convert = actions => { 5 | if (!Array.isArray(actions)) 6 | return convert([actions]); 7 | return actions.reduce((controller, item, i) => { 8 | if (typeof item === 'object' && Object.keys(item).length === 0) 9 | throw new Error(`[kelp-next] controller is empty`); 10 | const { method = '*', url: path } = item; 11 | const action = 12 | item.name || 13 | path || 14 | item.controller && item.controller.name || 15 | `action-${i}`; 16 | // obj like controller 17 | controller[action] = item.controller ? item.controller : item; 18 | controller.__routes.push({ method, path, action }); 19 | return controller; 20 | }, { __routes: [] }); 21 | }; 22 | 23 | module.exports = app => { 24 | const { controllers: dir } = app.config.path; 25 | app.registerController = (name, ctrl) => { 26 | app.controllers = app.controllers || {}; 27 | if (Array.isArray(ctrl.__routes)) { 28 | ctrl.__routes.forEach(route => { 29 | // because decorator can NOT get their filename name. 30 | route.controller = name; 31 | // when `route.path` is undefined, defaults to 32 | // controller name and action name 33 | if (route.path === undefined) { 34 | route.path = `/${route.controller}/${route.action}`; 35 | } 36 | // console.log(route.path); 37 | return app.registerRoute(route); 38 | }); 39 | } 40 | return app.controllers[name] = ctrl; 41 | }; 42 | // controllers 43 | app.loadController = filename => { 44 | var controller = app.import(filename); 45 | if (controller.__proto__ === Controller) { 46 | controller = new controller(app); 47 | } else if (typeof controller === 'function') { 48 | controller = convert(controller); 49 | } else if (Array.isArray(controller)) { 50 | controller = convert(controller); 51 | } else if (typeof controller === 'object') { 52 | controller = convert(controller); 53 | } else { 54 | throw new TypeError(`[kelp-next] controller must be a function at ${filename}`); 55 | } 56 | const name = filename.replace(`${dir}/`, '').replace(/\..+$/, ''); 57 | return app.registerController(name, controller); 58 | }; 59 | glob(`${dir}/**/*.{js,ts}`, (err, files) => { 60 | if (err) return console.error('[kelp-next] load controller error:', err); 61 | files.forEach(filename => app.loadController(filename)); 62 | }); 63 | return async (req, res, next) => { 64 | if (!req.route) return next(); 65 | const { controller: c, action: a } = req.route; 66 | const controller = app.controllers[c]; 67 | if (!controller) throw new Error(`[kelp-next] controller "${c}" not found`); 68 | const action = controller[a]; 69 | if (!action) throw new Error(`[kelp-next] action "${c}#${a}" not found`); 70 | res.scope = await res.invoke(action, controller); 71 | return next(); 72 | } 73 | }; -------------------------------------------------------------------------------- /core/plugin.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = app => { 4 | return app.middleware = app.config.plugins.map(plugin => { 5 | let name, args = []; 6 | if (typeof plugin === 'string') 7 | name = plugin; 8 | if (Array.isArray(plugin)) 9 | [name, ...args] = plugin; 10 | name && (plugin = app.import(name, [ 11 | app.config.path.plugins, 12 | path.join(__dirname, '../plugins') 13 | ])); 14 | return plugin.apply(app, [app].concat(args)); 15 | }).filter(mw => typeof mw === 'function'); 16 | }; -------------------------------------------------------------------------------- /core/responder.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = app => { 3 | return async (req, res, next) => { 4 | res.scope !== void (0) && res.send(res.scope); 5 | return next(); 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /core/router.js: -------------------------------------------------------------------------------- 1 | const { METHODS } = require('http') 2 | const routing = require('routing2'); 3 | 4 | const Router = app => { 5 | const { routes: rules } = app.config; 6 | app.routes = app.routes || []; 7 | app.router = METHODS.reduce((api, method) => { 8 | // app.router.get('/').to('home', 'index'); 9 | api[method.toLowerCase()] = path => { 10 | return { 11 | to(controller, action) { 12 | return app.registerRoute({ method, path, controller, action }) 13 | } 14 | }; 15 | }; 16 | return api; 17 | }, {}); 18 | app.registerRoute = route => { 19 | if (typeof route === 'string') 20 | route = routing.parseLine(route); 21 | app.routes.push(routing.create(route)); 22 | return route; 23 | }; 24 | // routes 25 | rules.forEach(app.registerRoute.bind(app)); 26 | return async (req, res, next) => { 27 | const { status, route } = routing.find(app.routes, req); 28 | res.statusCode = status; 29 | req.route = route; 30 | await next(); 31 | }; 32 | }; 33 | 34 | module.exports = Object.assign(Router, routing); -------------------------------------------------------------------------------- /core/service.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const injector = require('inject2'); 4 | const Import = require('../import'); 5 | 6 | module.exports = app => { 7 | const { services = {} } = app.config; 8 | const { services: dir } = app.config.path; 9 | app.import = Import; 10 | app.services = services; 11 | app.registerService = (name, service) => { 12 | app.services[name] = service; 13 | return service; 14 | }; 15 | app.loadService = filename => { 16 | const name = path.basename(filename).replace(/\..+$/, ''); 17 | const service = app.import(filename); 18 | return app.registerService(name, service); 19 | }; 20 | const readdir = dir => new Promise(resolve => { 21 | fs.readdir(dir, (err, files) => { 22 | resolve(err ? [] : files.map(f => path.join(dir, f))); 23 | }); 24 | }); 25 | Promise 26 | .all([ 27 | readdir(path.join(__dirname, '../services')), 28 | readdir(dir) 29 | ]) 30 | .then(files => [].concat.apply([], files)) 31 | .then(files => files.forEach(app.loadService.bind(app))) 32 | return async (req, res, next) => { 33 | res.invoke = injector(Object.assign({ 34 | // ... 35 | }, app.services, { req, res })); 36 | return next(); 37 | }; 38 | }; -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | 2 | ## API Reference 3 | 4 | + app.registerRoute 5 | + app.registerController -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Kanary Documents 2 | 3 | Kanary 是 [@kelpjs 项目](https://github.com/kelpjs) 的集合版本,它的目标是 “简单、快速、高效”。 4 | 5 | + [Get Started](./guide.md) 6 | + [Installation](./installation.md) 7 | + [Configuration](./configuration.md) 8 | + [Router](./router.md) 9 | + [Controller](./controller.md) 10 | + [Service](./service.md) 11 | + [Plugin/Middleware](./plugin.md) 12 | 13 | --- 14 | Kanary Project @ 2019 -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | 2 | ## Configuration 3 | 4 | 配置管理可以根据不同的运行环境改变 Kanary 的默认行为 5 | 6 | 默认配置: 7 | 8 | ```js 9 | const root = process.cwd(); 10 | 11 | export default { 12 | port: 3000, 13 | path: { 14 | root, 15 | public: path.join(root, 'public'), 16 | server: path.join(root, 'server'), 17 | models: path.join(root, 'server/models'), 18 | plugins: path.join(root, 'server/plugins'), 19 | services: path.join(root, 'server/services'), 20 | templates: path.join(root, 'server/templates'), 21 | controllers: path.join(root, 'server/controllers'), 22 | }, 23 | } 24 | ``` 25 | 26 | ### 自定义配置 27 | 28 | Kanary 会根据应用运行的环境 (NODE_ENV) 读取不同的配置文件 29 | 30 | 具体可以参考 [kelp-config](https://github.com/kelpjs/kelp-config) 的实现。 -------------------------------------------------------------------------------- /docs/controller.md: -------------------------------------------------------------------------------- 1 | ## Controller 2 | 3 | Kanary 中的 控制器 对于服务器来说非常重要,它是我们处理请求的关键。 4 | 5 | 目前支持以下类型: 6 | 7 | #### Function 函数 8 | 9 | ```js 10 | export default () => { 11 | return 'hello world'; 12 | }; 13 | ``` 14 | 15 | #### Object 对象 16 | 17 | ```js 18 | export default { 19 | method: 'get', 20 | url: '/:name?', 21 | controller(params) { 22 | const { name } = params; 23 | return `hello ${name}`; 24 | } 25 | }; 26 | 27 | ``` 28 | 29 | #### Array 数组 30 | 31 | 数组中可以包含上面两种类型 (Function and/or Object) 32 | 33 | ```js 34 | export default [ 35 | () => 'hello world 1', 36 | () => 'hello world 2', 37 | ]; 38 | ``` 39 | 40 | #### Class 类 41 | 42 | ```js 43 | import Controller from 'kanary/controller'; 44 | 45 | class Home extends Controller { 46 | async index(){ 47 | return 'hello world'; // 响应 48 | } 49 | } 50 | 51 | export default Home; 52 | ``` -------------------------------------------------------------------------------- /docs/guide.md: -------------------------------------------------------------------------------- 1 | # Get Started 2 | 3 | Kanary 相比目前社区中其他 Web 框架来说要更简单易用。 4 | 5 | 为了能更顺畅地进行后面的内容,我们假设你已经安装好了 [Node.js](https://nodejs.org) 并且有一定的编程基础。 6 | 7 | [📺 Watch Video Guide on YouTube](https://youtu.be/fuoSU5hJmMM) 8 | 9 | 首先我们创建一个新的项目,名字就叫做 "kanary-app" 吧 10 | 11 | ```bash 12 | ~$ mkdir kanary-app 13 | ~$ cd $_ 14 | ~$ git init 15 | ~$ npm init -y 16 | ``` 17 | 18 | 要使用 Kanary 框架,我们首先需要安装它: 19 | 20 | ```bash 21 | ~$ npm i kanary --save 22 | ``` 23 | 24 | 根据你的网络连接情况,这可能需要一点时间,不过一般很快就会安装好了。 25 | 26 | ## 使用 27 | 28 | 现在我们可以开始写一些代码来让我们的服务运行起来 29 | 30 | 创建一个文件 index.js 写入下面的代码: 31 | 32 | ```js 33 | require('kanary/start'); 34 | ``` 35 | 36 | 然后运行我们的服务器: 37 | 38 | ```bash 39 | ~$ node index.js 40 | ``` 41 | 42 | 现在打开浏览器访问 http://localhost:3000 可以看到它已经运行起来了。 43 | 44 | 虽然它现在看起来有点简陋,但是它确实运行起来了。 45 | 46 | ## 小结 47 | 48 | 这篇文章中我们介绍了如何创建一个 Node.js 项目,并安装 Kanary 框架。 49 | 50 | 与社区中大部分框架不同,Kanary 不需要复杂的设置、也不需要编写很多代码,只需要一行就可以使我们的服务运行。 51 | 52 | 这种设计思想会在 Kanary 框架中大量体现,后面你会 [了解更多](./README.md) 。 -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | Kanary 安装与大多数使用 NPM 托管代码的 Package 一样,只需要: 4 | 5 | ```bash 6 | ~$ npm i kanary --save 7 | ~$ # if you have "Yarn" ... 8 | ~$ yarn add kanary 9 | ``` -------------------------------------------------------------------------------- /docs/plugin.md: -------------------------------------------------------------------------------- 1 | ## Plugin / Middleware 2 | 3 | 插件 / 中间件 是 Kanary 中强大的扩展能力的体现,在插件中你可以对 Kanary 做扩展动作,也可以处理请求。 4 | 5 | 在 Kanary 中已经内置了大量 插件 / 中间件 可以做到开箱即用 6 | 7 | 插件会在项目启动时加载并执行一次 8 | 9 | 中间件会在每次请求时触发,并且有能力调度之后的中间件运行。 10 | 11 | ```js 12 | module.exports = (app, options) => { 13 | // plugin phase 14 | return async (req, res, next) => { 15 | // middleware phase 16 | await next(); 17 | }; 18 | }; 19 | ``` 20 | 21 | ### 如何编写插件 / 中间件 22 | 23 | ```js 24 | const AWS = require('aws-sdk'); 25 | 26 | module.exports = (app, options) => { 27 | const { endpoint, accessKey, secretKey } = options; 28 | app.s3 = new AWS.S3({ 29 | endpoint, 30 | accessKeyId: accessKey, 31 | secretAccessKey: secretKey 32 | }); 33 | return (req, res, next) => { 34 | req.s3 = app.s3; 35 | res.s3 = app.s3; 36 | return next(); 37 | }; 38 | }; 39 | ``` 40 | 41 | ### 内置插件 / 中间件 42 | 43 | + cookie 44 | + logger 45 | + render 46 | + static 47 | + database 48 | + mongodb -------------------------------------------------------------------------------- /docs/router.md: -------------------------------------------------------------------------------- 1 | ## Router 2 | 3 | 在 Kanary 中,所有路由都定义在 *app.routes* 数组中,当服务器收到请求时,**路由模块** 会查询请求匹配的路由规则并根据路由的描述调度 **控制器** 。 4 | 5 | 在 Kanary 中定义路由有几种方法: 6 | 7 | 1. 使用配置文件定义 8 | 2. 使用路由修饰器定义 9 | 3. 使用 [Kanary API](./API.md) 定义 10 | 11 | 下面我们依次介绍: 12 | 13 | ### 使用配置文件定义路由规则 14 | 15 | ```js 16 | exports.routes = [ 17 | 'get / => home#index' 18 | ]; 19 | ``` 20 | 21 | ### 使用修饰器定义路由 22 | 23 | ```js 24 | import { get } from 'kanary/router'; 25 | 26 | class Home { 27 | @get('/hi') 28 | async index(){ 29 | return 'hello world'; 30 | } 31 | } 32 | 33 | export default Home; 34 | ``` 35 | ### 使用 Kanary API 定义路由 36 | 37 | ```js 38 | module.exports = app => { 39 | app.router.get('/').to('home', 'index'); 40 | } 41 | ``` -------------------------------------------------------------------------------- /docs/service.md: -------------------------------------------------------------------------------- 1 | ## Service Injecting 2 | 3 | 依赖注入服务是 Kanary 中最有特色的功能之一,它能够让 [控制器](./controller.md) 处理请求时更加方便、高效。 4 | 5 | Kanary 中 Service 服务必须定义为 [Function 函数](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function), 该函数(即 "Service 服务") 可以依赖注入其他服务。并且返回任意类型的对象作为输出。 6 | 7 | ```js 8 | 9 | // ./server/services/demo.js 10 | 11 | module.exports = (req /* 注入 req 服务 */) => { 12 | // ... 13 | // 可以返回任意类型 14 | return {}; 15 | return 1; 16 | return () => {}; 17 | }; 18 | ``` 19 | 20 | 在 Controller 控制器中使用 Injecting 依赖注入 21 | 22 | ```js 23 | import Controller from 'kanary/controller'; 24 | 25 | class Home extends Controller { 26 | async index(demo){ 27 | return `hello ${demo}`; // 响应 28 | } 29 | } 30 | 31 | export default Home; 32 | ``` 33 | 34 | ### 内置的 Service 服务 35 | 36 | + req 37 | + res 38 | + query 39 | + params 40 | + headers 41 | + cookies 42 | + body 43 | + accepts -------------------------------------------------------------------------------- /example/config/default.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | routes: [ 3 | 'get /:name? => home#index' 4 | ], 5 | plugins: [ 6 | 'errorHandler', 7 | 'static', 8 | 'defaultHandler' 9 | ] 10 | }; -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | require('../start'); -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 |

Hello world

-------------------------------------------------------------------------------- /example/server/controllers/home.js: -------------------------------------------------------------------------------- 1 | import Controller from '../../../controller'; 2 | 3 | class Home extends Controller { 4 | async index(params){ 5 | const { name = 'world' } = params; 6 | return 'hello ' + name; 7 | } 8 | } 9 | 10 | module.exports = Home; -------------------------------------------------------------------------------- /example/server/templates/home.jade: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lsongdev/kanary/8a35fb3d35a3c768d06d22f225af458f9d55b8da/example/server/templates/home.jade -------------------------------------------------------------------------------- /import.js: -------------------------------------------------------------------------------- 1 | const Module = require('module'); 2 | 3 | const Import = (name, paths = module.paths) => { 4 | // https://github.com/nodejs/node/blob/master/lib/internal/modules/cjs/loader.js#L597 5 | const filename = Module._findPath(name, paths); 6 | if (!filename) { 7 | const err = new Error(`Cannot find module '${name}'`); 8 | err.code = 'MODULE_NOT_FOUND'; 9 | throw err; 10 | } 11 | const mod = require(filename); 12 | return mod.__esModule ? mod.default : mod; 13 | }; 14 | 15 | module.exports = Import; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const kelp = require('kelp'); 2 | const send = require('kelp-send'); 3 | const config = require('./core/config'); 4 | const router = require('./core/router'); 5 | const plugin = require('./core/plugin'); 6 | const service = require('./core/service'); 7 | const responder = require('./core/responder'); 8 | const controller = require('./core/controller'); 9 | 10 | module.exports = () => { 11 | const app = kelp(); 12 | app.use(send); 13 | app.use(config(app)); 14 | app.use(router(app)); 15 | app.use(service(app)); 16 | app.use(plugin(app)); 17 | app.use(controller(app)); 18 | app.use(responder(app)); 19 | return app; 20 | }; 21 | -------------------------------------------------------------------------------- /model.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize'); 2 | 3 | class Model extends Sequelize.Model { 4 | static get TYPES(){ 5 | return Sequelize; 6 | } 7 | } 8 | 9 | module.exports = Model; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kanary", 3 | "version": "2.0.9", 4 | "main": "index.js", 5 | "directories": { 6 | "doc": "docs", 7 | "example": "example", 8 | "test": "test" 9 | }, 10 | "keywords": [ 11 | "kanary", 12 | "framework" 13 | ], 14 | "dependencies": { 15 | "@babel/core": "7", 16 | "@babel/plugin-proposal-decorators": "7", 17 | "@babel/plugin-proposal-object-rest-spread": "7", 18 | "@babel/plugin-transform-modules-commonjs": "7", 19 | "@babel/preset-react": "7", 20 | "@babel/preset-typescript": "7", 21 | "@babel/register": "7", 22 | "glob": "latest", 23 | "inject2": "latest", 24 | "kelp": "latest", 25 | "kelp-body": "latest", 26 | "kelp-config": "latest", 27 | "kelp-cookie": "latest", 28 | "kelp-error": "latest", 29 | "kelp-logger": "latest", 30 | "kelp-render": "latest", 31 | "kelp-send": "latest", 32 | "kelp-static": "latest", 33 | "negotiator": "latest", 34 | "routing2": "latest", 35 | "semver": "latest" 36 | }, 37 | "description": "The next full-featured javascript frameworks.", 38 | "scripts": { 39 | "test": "node ./test" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "git+https://github.com/song940/kanary.git" 44 | }, 45 | "license": "MIT", 46 | "bugs": { 47 | "url": "https://github.com/song940/kanary/issues" 48 | }, 49 | "homepage": "https://github.com/song940/kanary#readme", 50 | "author": { 51 | "name": "Lsong", 52 | "email": "song940@gmail.com", 53 | "url": "https://lsong.org" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /plugins/cookie.js: -------------------------------------------------------------------------------- 1 | const cookie = require('kelp-cookie'); 2 | 3 | module.exports = () => cookie; -------------------------------------------------------------------------------- /plugins/database.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const Sequelize = require('sequelize'); 4 | 5 | module.exports = (app, options) => { 6 | const { database = options } = app.config; 7 | if (typeof database !== 'object') 8 | throw new TypeError('[kelp-next] database must be an object'); 9 | const db = new Sequelize(database); 10 | const { models: dir } = app.config.path; 11 | fs.readdir(dir, (err, files) => { 12 | if (err) throw err; 13 | db.models = files 14 | .map(file => path.join(dir, file)) 15 | .reduce((models, filename) => { 16 | const Model = app.import(filename); 17 | Model.init(Model.$schema, { 18 | sequelize: db 19 | }); 20 | models[Model.name] = Model; 21 | return models; 22 | }, {}); 23 | Object.keys(db.models).forEach(name => { 24 | if ('associate' in db.models[name]) { 25 | db.models[name].associate(db.models); 26 | } 27 | }); 28 | db.sync(); 29 | }); 30 | }; -------------------------------------------------------------------------------- /plugins/defaultHandler.js: -------------------------------------------------------------------------------- 1 | const NOT_FOUND = 404; 2 | 3 | module.exports = (app, options) => { 4 | return async (req, res, next) => { 5 | await next(); 6 | const { statusCode } = res; 7 | if(statusCode === NOT_FOUND) { 8 | res.writeHead(statusCode); 9 | res.end(); 10 | } 11 | }; 12 | }; -------------------------------------------------------------------------------- /plugins/errorHandler.js: -------------------------------------------------------------------------------- 1 | const error = require('kelp-error'); 2 | 3 | module.exports = () => error; -------------------------------------------------------------------------------- /plugins/logger.js: -------------------------------------------------------------------------------- 1 | const logger = require('kelp-logger'); 2 | 3 | module.exports = () => logger; -------------------------------------------------------------------------------- /plugins/mongodb.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const mongoose = require('mongoose'); 4 | 5 | module.exports = (app, options) => { 6 | let url, servers; 7 | ({ url, servers, ...options } = Object.assign({}, app.config.mongodb, options)); 8 | mongoose.connect(Array.isArray(servers) ? servers.join(',') : url, options); 9 | const { models: dir } = app.config.path; 10 | fs.readdirSync(dir) 11 | .map(file => path.join(dir, file)) 12 | .reduce((models, filename) => { 13 | const M = app.import(filename); 14 | const schema = new mongoose.Schema(M.$schema); 15 | const Model = mongoose.model(M.name, schema); 16 | require.cache[filename].exports = Model; // a magic hack ... 17 | models[M.name] = Model; 18 | // https://mongoosejs.com/docs/advanced_schemas.html 19 | schema.loadClass(M); 20 | return models; 21 | }, {}); 22 | }; -------------------------------------------------------------------------------- /plugins/oss.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | 3 | module.exports = (app, options) => { 4 | const { endpoint, accessKey, secretKey } = options; 5 | app.s3 = new AWS.S3({ 6 | endpoint, 7 | accessKeyId: accessKey, 8 | secretAccessKey: secretKey 9 | }); 10 | return (req, res, next) => { 11 | req.s3 = app.s3; 12 | res.s3 = app.s3; 13 | return next(); 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /plugins/render.js: -------------------------------------------------------------------------------- 1 | const render = require('../render'); 2 | 3 | module.exports = (app, options) => { 4 | return async (req, res, next) => { 5 | res.render = async (view, state, opts) => { 6 | const data = Object.assign({}, req.locals, state); 7 | const body = await render(view, data, opts); 8 | if (typeof body !== 'string') 9 | throw new TypeError(`[kelp-next] renderer must return a string`); 10 | res.end(body); 11 | }; 12 | return next(); 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /plugins/sentry.js: -------------------------------------------------------------------------------- 1 | const Sentry = require('node-sentry'); 2 | 3 | module.exports = (app, options) => { 4 | const sentry = app.sentry = new Sentry(options); 5 | return async (req, res, next) => { 6 | req.sentry = app.sentry; 7 | res.sentry = app.sentry; 8 | try { 9 | await next(); 10 | } catch(err) { 11 | sentry.captureException(err); 12 | } 13 | }; 14 | }; -------------------------------------------------------------------------------- /plugins/static.js: -------------------------------------------------------------------------------- 1 | const serve = require('kelp-static'); 2 | 3 | module.exports = (app, root, options) => { 4 | root = root || app.config.path.public; 5 | return serve(root, options); 6 | }; -------------------------------------------------------------------------------- /react.js: -------------------------------------------------------------------------------- 1 | const ssr = require('kelp-ssr'); 2 | const render = require('./render'); 3 | const $import = require('./import'); 4 | 5 | const react = (page, state, options) => { 6 | const Page = $import(`@client/pages/${page}`); 7 | const html = ssr(Page, state, options); 8 | return render('react', { page, html }); 9 | }; 10 | 11 | module.exports = react; -------------------------------------------------------------------------------- /render.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const config = require('./config'); 3 | const Import = require('./import'); 4 | const render = require('kelp-render/render'); 5 | 6 | const cache = new Map(); 7 | const { templates } = config.path; 8 | 9 | const defaultOptions = { 10 | cache, 11 | renderer: 'default', 12 | templates, 13 | extension: '', 14 | }; 15 | 16 | const createEngine = name => { 17 | if (typeof name === 'function') 18 | return { renderer: name }; 19 | return Import(name, [ 20 | path.join(__dirname, './renders') 21 | ]); 22 | }; 23 | 24 | const createRender = options => { 25 | if (typeof options === 'string') 26 | options = { renderer: options }; 27 | options = Object.assign({}, defaultOptions, options); 28 | const engine = createEngine(options.renderer); 29 | options = Object.assign({}, options, engine); 30 | return render(options); 31 | }; 32 | 33 | module.exports = createRender(config.render); -------------------------------------------------------------------------------- /renders/default.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = 3 | require('./ejs'); -------------------------------------------------------------------------------- /renders/ejs.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const ejs = require('ejs'); 3 | const { promisify } = require('util'); 4 | 5 | const readFile = promisify(fs.readFile); 6 | 7 | module.exports = { 8 | extension: 'ejs', 9 | renderer: async (filename, options = {}) => { 10 | const { encoding = 'utf8' } = options; 11 | const str = await readFile(filename, encoding); 12 | return ejs.compile(str, options); 13 | } 14 | }; -------------------------------------------------------------------------------- /renders/handlebars.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { promisify } = require('util'); 3 | const Handlebars = require('handlebars'); 4 | 5 | const readFile = promisify(fs.readFile); 6 | 7 | module.exports = { 8 | extension: 'hbs', 9 | renderer: async (filename, options = {}) => { 10 | const { encoding = 'utf8' } = options; 11 | const str = await readFile(filename, encoding); 12 | return Handlebars.compile(str, options); 13 | } 14 | }; -------------------------------------------------------------------------------- /renders/jade.js: -------------------------------------------------------------------------------- 1 | const jade = require('jade'); 2 | 3 | module.exports = { 4 | extension: 'jade', 5 | renderer: jade.compileFile 6 | }; -------------------------------------------------------------------------------- /renders/nunjucks.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { promisify } = require('util'); 3 | const nunjucks = require('nunjucks'); 4 | 5 | const readFile = promisify(fs.readFile); 6 | 7 | module.exports = { 8 | extension: 'njk', 9 | renderer: async (filename, options = {}) => { 10 | const { encoding = 'utf8', templates } = options; 11 | const env = nunjucks.configure(templates, options); 12 | const str = await readFile(filename, encoding); 13 | const fn = nunjucks.compile(str, env, filename); 14 | return fn.render.bind(fn); 15 | } 16 | }; -------------------------------------------------------------------------------- /response.js: -------------------------------------------------------------------------------- 1 | const { Cookie } = require('kelp-cookie'); 2 | const Negotiator = require('negotiator'); 3 | const BaseResponse = require('kelp-send/response'); 4 | 5 | class Response extends BaseResponse { 6 | static create(options) { 7 | return new Response(options); 8 | } 9 | constructor() { 10 | super(); 11 | this.code = 200; 12 | this.headers = {}; 13 | this.cookies = {}; 14 | this.contents = []; 15 | } 16 | html(view, data, options) { 17 | this.contents.push({ 18 | type: 'text/html', 19 | fn: (req, res) => res.render(view, data, options) 20 | }); 21 | return this; 22 | } 23 | json(obj) { 24 | this.contents.push({ 25 | type: 'application/json', 26 | fn: (req, res) => res.end(JSON.stringify(obj)) 27 | }); 28 | return this; 29 | } 30 | text(content) { 31 | this.contents.push({ 32 | type: 'text/plain', 33 | fn: (req, res) => res.end(content) 34 | }); 35 | return this; 36 | } 37 | header(key, value) { 38 | this.headers[key] = value; 39 | return this; 40 | } 41 | status(code) { 42 | this.code = code; 43 | return this; 44 | } 45 | cookie(key, value, options) { 46 | const cookie = new Cookie(key, value, options); 47 | this.headers['Set-Cookie'] = cookie.toHeader(); 48 | return this; 49 | } 50 | redirect(url, code = 302) { 51 | this.code = code; 52 | this.headers.Location = url; 53 | return this; 54 | } 55 | respond(req, res) { 56 | const negotiator = new Negotiator(req); 57 | const availableMediaTypes = this.contents.map(content => content.type); 58 | const responseType = negotiator.mediaType(availableMediaTypes); 59 | const index = this.contents.findIndex(x => x.type === responseType); 60 | if (~index) { 61 | res.writeHead(this.code, Object.assign(this.headers, { 62 | 'content-type': responseType 63 | })); 64 | this.contents[index].fn(req, res); 65 | } else { 66 | res.writeHead(this.code, this.headers); 67 | res.end(); 68 | } 69 | } 70 | } 71 | 72 | module.exports = Response; 73 | -------------------------------------------------------------------------------- /router.js: -------------------------------------------------------------------------------- 1 | const { METHODS } = require('http'); 2 | const Router = require('./core/router'); 3 | 4 | Router.route = (method = '*', path = '/', options = {}) => { 5 | return (controller, action) => { 6 | controller.__routes = controller.__routes || []; 7 | controller.__routes.push({ 8 | method, path, action, options 9 | }); 10 | }; 11 | }; 12 | 13 | // alias 14 | METHODS.forEach(method => { 15 | Router[method.toLowerCase()] = Router.route.bind(null, method); 16 | }); 17 | 18 | Router.all = Router.route.bind(null, null); 19 | Router.restful = (path, options) => { 20 | const methods = { 21 | get: 'index', 22 | put: 'update', 23 | post: 'create', 24 | delete: 'remove' 25 | }; 26 | return target => { 27 | path = (path || `/${target.name}`).toLowerCase(); 28 | path += '/:id?'; 29 | target.prototype.__routes = target.prototype.__routes || []; 30 | Object.keys(methods).forEach(method => { 31 | target.prototype.__routes.push({ 32 | method, path, action: methods[method], options 33 | }); 34 | }); 35 | }; 36 | }; 37 | 38 | /** 39 | * prefix 40 | */ 41 | Router.prefix = (prefix = '') => { 42 | if (typeof prefix !== 'string') 43 | throw new TypeError(`[kelp-next] "prefix" must a string, but got a "${typeof prefix}".`) 44 | return ctrl => { 45 | if (!ctrl.prototype.__routes) 46 | throw new Error(`[kelp-next] controller "${ctrl.name}" haven't set router yet, you may need to call functions like "@get", "@post", "@restfull".`); 47 | ctrl.prototype.__routes.forEach(route => { 48 | route.path = prefix + route.path; 49 | }); 50 | }; 51 | }; 52 | 53 | module.exports = Router; 54 | -------------------------------------------------------------------------------- /services/accepts.js: -------------------------------------------------------------------------------- 1 | const Negotiator = require('negotiator'); 2 | 3 | module.exports = req => { 4 | const negotiator = new Negotiator(req); 5 | return { 6 | negotiator, 7 | types: negotiator.mediaTypes(), 8 | encodings: negotiator.encodings(), 9 | languages: negotiator.languages(), 10 | }; 11 | } -------------------------------------------------------------------------------- /services/body.js: -------------------------------------------------------------------------------- 1 | const parse = require('kelp-body'); 2 | 3 | module.exports = req => 4 | new Promise((resolve, reject) => { 5 | parse(req, null, err => { 6 | if (err) return reject(err); 7 | resolve(req.body); 8 | }); 9 | }); -------------------------------------------------------------------------------- /services/cookies.js: -------------------------------------------------------------------------------- 1 | const { parse } = require('kelp-cookie'); 2 | 3 | module.exports = headers => { 4 | const { cookie } = headers; 5 | return parse(cookie); 6 | } -------------------------------------------------------------------------------- /services/headers.js: -------------------------------------------------------------------------------- 1 | module.exports = req => 2 | req.headers; -------------------------------------------------------------------------------- /services/ip.js: -------------------------------------------------------------------------------- 1 | const IPReg = /\d+.\d+.\d+.\d+/; 2 | 3 | module.exports = (req, headers) => { 4 | return [ 5 | headers['x-real-ip'], 6 | headers['x-forwarded-for'], 7 | req.connection.remoteAddress, 8 | '127.0.0.1' 9 | ] 10 | .filter(Boolean) 11 | .filter(ip => IPReg.test(ip)) 12 | .map(ip => IPReg.exec(ip)[0])[0]; 13 | }; -------------------------------------------------------------------------------- /services/params.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = req => 3 | req.route.params; -------------------------------------------------------------------------------- /services/query.js: -------------------------------------------------------------------------------- 1 | const url = require('url'); 2 | 3 | module.exports = req => 4 | url.parse(req.url, true).query; -------------------------------------------------------------------------------- /start.js: -------------------------------------------------------------------------------- 1 | require('./check'); 2 | require('./babel'); 3 | require('./alias'); 4 | 5 | const os = require('os'); 6 | const http = require('http'); 7 | const cluster = require('cluster'); 8 | 9 | const kelp = require('.'); 10 | 11 | const app = kelp(); 12 | 13 | const { config } = app; 14 | 15 | let { workers } = config; 16 | if (~[true, 0, 'auto'].indexOf(workers)) { 17 | const cpus = os.cpus(); 18 | workers = cpus.length; 19 | } 20 | 21 | const start = () => { 22 | while (workers && Object.keys(cluster.workers).length < workers) { 23 | cluster.fork(); 24 | } 25 | }; 26 | 27 | cluster.on('online', worker => { 28 | console.log('[@kelpjs/next] cluster worker started', worker.id); 29 | }); 30 | 31 | cluster.on('exit', (worker, code, signal) => { 32 | if (signal) { 33 | console.warn(`[@kelpjs/next] worker ${worker.id} was killed by signal: ${signal}`); 34 | } else if (code !== 0) { 35 | console.error(`[@kelpjs/next] worker ${worker.id} exited with error code: ${code}`); 36 | start(); 37 | } else { 38 | console.log(`[@kelpjs/next] cluster worker ${worker.id} exited`); 39 | } 40 | }); 41 | 42 | // cluster mode 43 | if (cluster.isMaster) { 44 | cluster.setupMaster({ 45 | exec: __filename 46 | }); 47 | start(); 48 | } 49 | 50 | const server = http.createServer(app); 51 | 52 | if (!workers || cluster.isWorker) { 53 | server.listen(config.port, () => { 54 | console.log('[@kelpjs/next] server is running at %s', server.address().port); 55 | }); 56 | } 57 | 58 | app.server = server; 59 | 60 | module.exports = app; 61 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lsongdev/kanary/8a35fb3d35a3c768d06d22f225af458f9d55b8da/test/index.js -------------------------------------------------------------------------------- /worker.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lsongdev/kanary/8a35fb3d35a3c768d06d22f225af458f9d55b8da/worker.js --------------------------------------------------------------------------------