├── .gitignore ├── packages ├── cli │ ├── loader.ts │ ├── logger.ts │ ├── resolver.ts │ └── index.ts └── core │ ├── index.ts │ └── lib │ ├── utils │ ├── base_context_class.ts │ ├── timing.ts │ ├── sequencify.ts │ └── index.ts │ ├── loader │ ├── mixin │ │ ├── router.ts │ │ ├── service.ts │ │ ├── custom.ts │ │ ├── custom_loader.ts │ │ ├── config.ts │ │ ├── middleware.ts │ │ ├── controller.ts │ │ ├── extend.ts │ │ └── plugin.ts │ ├── context_loader.ts │ ├── file_loader.ts │ └── egg_loader.ts │ ├── egg.ts │ └── lifecycle.ts ├── config ├── config.default.js └── config.project.js ├── tools ├── create-readme │ ├── contributors.md │ ├── header.md │ ├── useAndIntsall.md │ └── index.js ├── request-promise │ └── index.js └── issue-readme │ ├── index.js │ └── config.json ├── demo ├── koa │ ├── kao-demo.md │ ├── run.ts │ └── extend │ │ ├── context.ts │ │ ├── application.ts │ │ ├── response.ts │ │ └── request.ts └── koa-router │ ├── run.ts │ ├── Layer.ts │ └── router.ts ├── .DS_Store ├── app ├── routers │ └── routers.md ├── run.ts └── server.ts ├── commitlint.config.js ├── docs ├── 01-搭建项目基础.md ├── 00-node面试题.md └── 02-封装koa|koa-router.md ├── .vscode ├── launch.json └── settings.json ├── .editorconfig ├── nodemon.json ├── tsconfig.json ├── prettier.config.js ├── .eslintrc.js ├── package.json ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /packages/cli/loader.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/cli/logger.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/config.default.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/cli/resolver.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tools/create-readme/contributors.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/koa/kao-demo.md: -------------------------------------------------------------------------------- 1 | ## 实现简易的koa 2 | 3 | -------------------------------------------------------------------------------- /tools/create-readme/header.md: -------------------------------------------------------------------------------- 1 | ## 学习 node 开始 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luoxue-victor/learn-node/HEAD/.DS_Store -------------------------------------------------------------------------------- /app/routers/routers.md: -------------------------------------------------------------------------------- 1 | ## routers 2 | 3 | 这里会把所有的 routers 内部的文件自动添加到 koa-router 中 -------------------------------------------------------------------------------- /app/run.ts: -------------------------------------------------------------------------------- 1 | import Server from './server' 2 | 3 | const app = new Server() 4 | 5 | app.start() 6 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['./node_modules/vue-cli-plugin-commitlint/lib/lint'] 3 | } 4 | -------------------------------------------------------------------------------- /packages/cli/index.ts: -------------------------------------------------------------------------------- 1 | import Koa from 'koa' 2 | // import Router from 'koa-router' 3 | 4 | export default class XueTang extends Koa { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /tools/create-readme/useAndIntsall.md: -------------------------------------------------------------------------------- 1 | ## 安装 2 | 3 | ```bash 4 | git clone https://github.com/luoxue-victor/learn-node.git 5 | cd learn-node 6 | yarn install 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/01-搭建项目基础.md: -------------------------------------------------------------------------------- 1 | ## 搭建项目基础 2 | 3 | - 代码提交检查 npm run cz 4 | - eslint 配置 + fix 5 | - vscode 配置 6 | - 自动生成 readme 7 | - 自动获取获取 issue,并按 label 分类添加到 readme 8 | - 支持 ts,使用 ts-node 9 | - 编辑器风格统一配置 10 | - prettier 代码风格设置 11 | - 热更新 nodemon 12 | - vscode + debug,一键断点调试 13 | -------------------------------------------------------------------------------- /packages/core/index.ts: -------------------------------------------------------------------------------- 1 | import EggCore from './lib/egg' 2 | import Loader from './lib/loader/egg_loader' 3 | import BaseContextClass from './lib/utils/base_context_class' 4 | import utils from './lib/utils' 5 | 6 | export default { 7 | EggCore, 8 | Loader, 9 | BaseContextClass, 10 | utils 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/lib/utils/base_context_class.ts: -------------------------------------------------------------------------------- 1 | export default class BaseContextClass { 2 | ctx: any 3 | app: any 4 | config: any 5 | service: any 6 | constructor (ctx) { 7 | this.ctx = ctx 8 | this.app = ctx.app 9 | this.config = ctx.app.config 10 | this.service = ctx.service 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/server.ts: -------------------------------------------------------------------------------- 1 | import Application from '../demo/koa/extend/application' 2 | const app = new Application() 3 | 4 | export default class Server { 5 | start () { 6 | app.use((ctx) => { 7 | ctx.body = 'hello world' 8 | }) 9 | 10 | app.listen(3000, () => { 11 | console.log('listening on 3000') 12 | console.log('app run at: http://127.0.0.1:3000/') 13 | }) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /demo/koa-router/run.ts: -------------------------------------------------------------------------------- 1 | import Application from '../koa/extend/application' 2 | 3 | import Router from './router' 4 | 5 | const app = new Application() 6 | const router = new Router() 7 | 8 | router.get('/', async (ctx) => { 9 | ctx.body = '首页' 10 | }) 11 | 12 | app.use(router.routes()) 13 | app.use(router.allowedMethods()) 14 | 15 | app.listen(3000) 16 | 17 | console.log('app run at: http://127.0.0.1:3000/') 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.1", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Launch Program", 8 | "runtimeExecutable": "npm", 9 | "runtimeArgs": ["run", "debug"], 10 | "cwd": "${workspaceRoot}", 11 | "port": 3000, 12 | "console": "integratedTerminal", 13 | "sourceMaps": true 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space # 输入的 tab 都用空格代替 5 | indent_size = 2 # 一个 tab 用 2 个空格代替 6 | end_of_line = lf # 换行符使用 unix 的换行符 \n 7 | charset = utf-8 # 字符编码 utf-8 8 | trim_trailing_whitespace = true # 去掉每行末尾的空格 9 | insert_final_newline = true # 每个文件末尾都加一个空行 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false # .md 文件不去掉每行末尾的空格 -------------------------------------------------------------------------------- /demo/koa/run.ts: -------------------------------------------------------------------------------- 1 | import Application from './extend/application' 2 | const app = new Application() 3 | 4 | console.log('koa-demo') 5 | 6 | export default class Server { 7 | start () { 8 | app.use((ctx) => { 9 | ctx.body = 'hello world' 10 | }) 11 | 12 | app.listen(3000, () => { 13 | console.log('listening on 3000') 14 | console.log('app run at: http://127.0.0.1:3000/') 15 | }) 16 | } 17 | } 18 | 19 | const server = new Server() 20 | 21 | server.start() 22 | -------------------------------------------------------------------------------- /packages/core/lib/loader/mixin/router.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | export default { 4 | 5 | /** 6 | * Load app/router.js 7 | * @function EggLoader#loadRouter 8 | * @param {Object} opt - LoaderOptions 9 | * @since 1.0.0 10 | */ 11 | loadRouter () { 12 | (this as any).timing.start('Load Router'); 13 | // 加载 router.js 14 | (this as any).loadFile(path.join((this as any).options.baseDir, 'app/router')); 15 | (this as any).timing.end('Load Router') 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "restartable": "rs", 3 | "ignore": [ 4 | ".git", 5 | ".vscode", 6 | ".idea", 7 | "node_modules/", 8 | "test/", 9 | "src/" 10 | ], 11 | "verbose": true, 12 | "execMap": { 13 | "": "node", 14 | "js": "node --harmony" 15 | }, 16 | "events": { 17 | "restart": "osascript -e 'display notification \"App restarted due to:\n'$FILENAME'\" with title \"nodemon\"'" 18 | }, 19 | "watch": [ 20 | "build/" 21 | ], 22 | "ext": "js json", 23 | "legacy-watch": false 24 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "resolveJsonModule": true, 5 | "esModuleInterop": true, 6 | "target": "es6", 7 | "noImplicitAny": false, 8 | "allowJs": true, 9 | "baseUrl": ".", 10 | "outDir": "dist", 11 | "sourceMap": false, 12 | "moduleResolution": "node", 13 | "noUnusedLocals": true, 14 | "strictNullChecks": false, 15 | "noImplicitThis": true, 16 | "experimentalDecorators": true, 17 | "removeComments": false, 18 | "lib": ["esnext", "dom"], 19 | "rootDir": ".", 20 | "paths": {} 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/lib/loader/mixin/service.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | export default { 4 | 5 | loadService (opt) { 6 | (this as any).timing.start('Load Service') 7 | // 载入到 app.serviceClasses 8 | opt = Object.assign({ 9 | call: true, 10 | caseStyle: 'lower', 11 | fieldClass: 'serviceClasses', 12 | directory: (this as any).getLoadUnits().map(unit => path.join(unit.path, 'app/service')) 13 | }, opt) 14 | const servicePaths = opt.directory; 15 | (this as any).loadToContext(servicePaths, 'service', opt); 16 | (this as any).timing.end('Load Service') 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /config/config.project.js: -------------------------------------------------------------------------------- 1 | export default { 2 | port: '8083', 3 | log: { 4 | log_name: 'log', 5 | log_path: 'logs/' 6 | }, 7 | mongo: { 8 | development: { 9 | host: 'mongodb://localhost:27017/ts-test' 10 | }, 11 | production: { 12 | host: '' 13 | } 14 | }, 15 | redis: { 16 | development: { 17 | host: 'localhost', 18 | db: 1 19 | }, 20 | production: { 21 | host: '', 22 | db: 1 23 | } 24 | }, 25 | session: { 26 | secrets: 'koa-ts' 27 | }, 28 | jwt: { 29 | secret: 'koa-ts-jwt', 30 | key: 'mytoken', 31 | time: '1d' 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tools/request-promise/index.js: -------------------------------------------------------------------------------- 1 | 2 | const request = require('request') 3 | const qs = require('querystring') 4 | 5 | exports.requestPromise = function requestPromise (url, params) { 6 | return new Promise((resolve, reject) => { 7 | request({ 8 | url: `${url}?${qs.stringify(params)}`, 9 | method: 'GET', 10 | json: true, 11 | headers: { 12 | 'User-Agent': 13 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + 14 | ' (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36' 15 | } 16 | }, (error, response, body) => { 17 | if (error) reject(error) 18 | if (!error && response.statusCode === 200) { 19 | resolve(body) 20 | } else { 21 | reject(body) 22 | } 23 | }) 24 | }) 25 | } -------------------------------------------------------------------------------- /docs/00-node面试题.md: -------------------------------------------------------------------------------- 1 | ## 面试题 2 | 3 | - 如何实现 promise.map,并限制并发数 4 | - Node 如何进行进程间通信 5 | - 有没有用过 continuous local storage,用在了哪里 6 | - 在 Node 应用中如何利用多核心 CPU 的优势 7 | - 如何设计一个高并发系统 8 | - 什么是熔断机制,微服务如何做熔断 9 | - 什么是负载均衡 10 | - 四层负载均衡与七层负载均衡有什么区别 11 | - 你们项目中的计划任务是如何组织的 12 | - RPC 与 REST 有什么优劣 13 | - 如何实现服务发现 (Service Discovery) 14 | - node 中的 Buffer 如何应用 15 | - Node.js 框架同其他语言框架的比较 16 | - Node.js 与 Swift 在 Web 领域的未来? 17 | - 说一下关于 Node.js 的文件读写方式和实现 18 | - 说一下 JavaScript 几种异步方法和原理 19 | - 介绍一下 Session 和 Cookie? 20 | - docker 会用吗? 21 | - 说一下事件循环 eventloop 22 | - node 怎么跟 MongoDB 建立连接 23 | - node 和 前端项目怎么解决跨域的 24 | 25 | ### 设计题 26 | 27 | - 设计一个简单的红绿灯策略,比如红灯亮分别为 console.log(“red”)这种,要求按照红 3s-黄 1s-绿 1s 顺序不断循环展示。 28 | 29 | ### 算法题 30 | 31 | - 给定一个整数金额的整钱 n,还有 2,3,5 元三种货币,要你计算出所有能凑出整钱的组合个数 32 | - 假如这个能使用的货币列表是给定的,意思是输入一个整数 list,比如[1,2,3,5],还有金额 n,求出所有组合数。 33 | 34 | ### 优质文章 35 | 36 | https://blog.csdn.net/MingL520/article/details/105193463/ 37 | -------------------------------------------------------------------------------- /packages/core/lib/utils/timing.ts: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const MAP = Symbol('Timing#map') 3 | const LIST = Symbol('Timing#list') 4 | 5 | export default class Timing { 6 | constructor () { 7 | this[MAP] = new Map() 8 | this[LIST] = [] 9 | } 10 | 11 | start (name) { 12 | if (!name) return 13 | 14 | if (this[MAP].has(name)) this.end(name) 15 | 16 | const start = Date.now() 17 | const item = { 18 | name, 19 | start, 20 | end: undefined, 21 | duration: undefined, 22 | pid: process.pid, 23 | index: this[LIST].length 24 | } 25 | this[MAP].set(name, item) 26 | this[LIST].push(item) 27 | return item 28 | } 29 | 30 | end (name) { 31 | if (!name) return 32 | assert(this[MAP].has(name), `should run timing.start('${name}') first`) 33 | 34 | const item = this[MAP].get(name) 35 | item.end = Date.now() 36 | item.duration = item.end - item.start 37 | return item 38 | } 39 | 40 | toJSON () { 41 | return this[LIST] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * pretiier 标准配置 3 | */ 4 | module.exports = { 5 | // 在ES5中有效的结尾逗号(对象,数组等) 6 | trailingComma: 'es5', 7 | // 不使用缩进符,而使用空格 8 | useTabs: false, 9 | // tab 用两个空格代替 10 | tabWidth: 2, 11 | // 仅在语法可能出现错误的时候才会添加分号 12 | semi: false, 13 | // 使用单引号 14 | singleQuote: true, 15 | // 在Vue文件中缩进脚本和样式标签。 16 | vueIndentScriptAndStyle: true, 17 | // 一行最多 100 字符 18 | printWidth: 100, 19 | // 对象的 key 仅在必要时用引号 20 | quoteProps: 'as-needed', 21 | // jsx 不使用单引号,而使用双引号 22 | jsxSingleQuote: false, 23 | // 大括号内的首尾需要空格 24 | bracketSpacing: true, 25 | // jsx 标签的反尖括号需要换行 26 | jsxBracketSameLine: false, 27 | // 箭头函数,只有一个参数的时候,也需要括号 28 | arrowParens: 'always', 29 | // 每个文件格式化的范围是文件的全部内容 30 | rangeStart: 0, 31 | rangeEnd: Infinity, 32 | // 不需要写文件开头的 @prettier 33 | requirePragma: false, 34 | // 不需要自动在文件开头插入 @prettier 35 | insertPragma: false, 36 | // 使用默认的折行标准 37 | proseWrap: 'preserve', 38 | // 根据显示样式决定 html 要不要折行 39 | htmlWhitespaceSensitivity: 'css', 40 | // 换行符使用 lf 41 | endOfLine: 'lf' 42 | } 43 | -------------------------------------------------------------------------------- /docs/02-封装koa|koa-router.md: -------------------------------------------------------------------------------- 1 | ## Koa 搭建 2 | 3 | ### RUN 4 | 5 | ```bash 6 | npm run demo-koa # koa demo 7 | npm run demo-koa-router # koa-router demo 8 | ``` 9 | 10 | ### 实现 koa 11 | 12 | ```js 13 | import Application from './extend/application' 14 | const app = new Application() 15 | 16 | console.log('koa-demo') 17 | 18 | export default class Server { 19 | start () { 20 | app.use((ctx) => { 21 | ctx.body = 'hello world' 22 | }) 23 | 24 | app.listen(3000, () => { 25 | console.log('listening on 3000') 26 | console.log('app run at: http://127.0.0.1:3000/') 27 | }) 28 | } 29 | } 30 | 31 | const server = new Server() 32 | 33 | server.start() 34 | ``` 35 | 36 | ### 实现 koa-router 37 | 38 | ```js 39 | import Application from '../koa/extend/application' 40 | 41 | import Router from './router' 42 | 43 | const app = new Application() 44 | const router = new Router() 45 | 46 | router.get('/', async (ctx) => { 47 | ctx.body = '首页' 48 | }) 49 | 50 | app.use(router.routes()) 51 | app.use(router.allowedMethods()) 52 | 53 | app.listen(3000) 54 | 55 | console.log('app run at: http://127.0.0.1:3000/') 56 | ``` -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.validate.enable": false, 3 | /* 4 | * @description 编译器配置 5 | * @param tabSize 默认tab为两个空格 6 | * @param formatOnSave 保存时自动修复 7 | */ 8 | "editor.tabSize": 2, 9 | // "editor.formatOnSave": true, 10 | /* 11 | * @description eslint 配置 12 | * @param alwaysShowStatus 配置 13 | * @param autoFixOnSave 保存时自动修复 14 | * @param validate 在vue中添加错误提示 15 | */ 16 | "eslint.alwaysShowStatus": true, 17 | /* 18 | * @description vetur 配置 19 | */ 20 | "vetur.format.defaultFormatter.html": "prettier", 21 | "vetur.format.defaultFormatterOptions": { 22 | "prettier": { 23 | "semi": false, 24 | "singleQuote": true 25 | } 26 | }, 27 | /* 28 | * @description 配置编辑器设置以覆盖某种语言 29 | */ 30 | "[html]": { 31 | "editor.defaultFormatter": "esbenp.prettier-vscode" 32 | }, 33 | "[jsonc]": { 34 | "editor.defaultFormatter": "esbenp.prettier-vscode" 35 | }, 36 | "[json]": { 37 | "editor.defaultFormatter": "esbenp.prettier-vscode" 38 | }, 39 | "[javascript]": { 40 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 41 | }, 42 | "[javascriptreact]": { 43 | "editor.defaultFormatter": "esbenp.prettier-vscode" 44 | }, 45 | "editor.codeActionsOnSave": { 46 | "source.fixAll.eslint": true 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tools/create-readme/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const readmePath = path.join('tools', 'create-readme') 4 | 5 | console.log('--- 创建readme ---') 6 | 7 | const docsCtx = extraTxt('docs', (firstRow) => { 8 | return `[${firstRow.replace('## ', '')}]` 9 | }) 10 | 11 | function joinCtx () { 12 | let str = '' 13 | str += readMdBy('header') 14 | str += detailTag('所有课题', docsCtx, true) 15 | str += readMdBy('useAndIntsall') 16 | str += '\n' + 17 | fs.readFileSync(path.join(__dirname, 'contributors.md')).toString() 18 | return str 19 | } 20 | 21 | const ctx = joinCtx() 22 | 23 | fs.writeFileSync('README.md', ctx, 'utf-8') 24 | 25 | function detailTag (title, ctx, isOpen = true) { 26 | return ` 27 | ## ${title} 28 |
29 | 点击关闭/打开${title} 30 |
31 | \n\n${ctx} 32 |
\n\n` 33 | } 34 | 35 | function extraTxt (dirname, firstRowStrategy) { 36 | const files = fs.readdirSync(dirname) 37 | let ctx = '' 38 | files.forEach(file => { 39 | const absolutePath = path.join(process.cwd(), dirname, file) 40 | if (fs.statSync(absolutePath).isDirectory()) return 41 | const content = fs.readFileSync(absolutePath).toString() 42 | const firstRow = content.split('\n')[0].trim() 43 | const title = firstRowStrategy(firstRow) 44 | ctx += `- ${title}(./${dirname}/${file})\n` 45 | }) 46 | return ctx 47 | } 48 | 49 | function readMdBy (name) { 50 | return fs.readFileSync(path.join(readmePath, name + '.md')).toString() 51 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', // eslint 识别此选项 4 | parserOptions: { 5 | ecmaVersion: 6, // 指定ECMAScript的版本为 6 6 | parser: '@typescript-eslint/parser', // 解析 ts 7 | sourceType: 'module' 8 | }, 9 | // 全局变量 10 | globals: { 11 | window: true, 12 | document: true, 13 | __dirname: true, 14 | process: true 15 | }, 16 | // 兼容环境 17 | env: { 18 | browser: true 19 | }, 20 | // 插件 21 | extends: ['standard'], 22 | // 规则 23 | rules: { 24 | // 末尾不加分号,只有在有可能语法错误时才会加分号 25 | semi: 'warn', 26 | // 箭头函数需要有括号 (a) => {} 27 | 'arrow-parens': 0, 28 | // 两个空格缩进, switch 语句中的 case 为 1 个空格 29 | indent: [ 30 | 'error', 31 | 2, 32 | { 33 | SwitchCase: 1 34 | } 35 | ], 36 | 'no-useless-escape': 'off', 37 | // 关闭副作用的 new 38 | 'no-new': 'off', 39 | // 每行最大长度小于 80,忽略注释 40 | // 'max-len': ['error', { ignoreComments: true, "comments": 120 }], 41 | // 关闭要求 require() 出现在顶层模块作用域中 42 | 'global-require': 0, 43 | // 关闭类方法中必须使用this 44 | 'class-methods-use-this': 0, 45 | // 禁止对原生对象或只读的全局对象进行赋值 46 | 'no-global-assign': 0, 47 | // 禁止对关系运算符的左操作数使用否定操作符 48 | 'no-unsafe-negation': 0, 49 | // 禁止末尾空行 50 | 'eol-last': 1, 51 | // 关闭禁止对 function 的参数进行重新赋值 52 | 'no-param-reassign': 0, 53 | // 关闭要求构造函数首字母大写 54 | 'new-cap': 0, 55 | // 全等校验 56 | eqeqeq: 'error', 57 | // 禁止使用拖尾逗号 58 | 'comma-dangle': ['error', 'never'], 59 | // 关闭强制使用骆驼拼写法命名约定 60 | camelcase: 0 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/core/lib/loader/mixin/custom.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | const is = require('is-type-of') 3 | 4 | const LOAD_BOOT_HOOK = Symbol('Loader#loadBootHook') 5 | 6 | export default { 7 | loadCustomApp () { 8 | this[LOAD_BOOT_HOOK]('app'); 9 | (this as any).lifecycle.triggerConfigWillLoad() 10 | }, 11 | 12 | /** 13 | * Load agent.js, same as {@link EggLoader#loadCustomApp} 14 | */ 15 | loadCustomAgent () { 16 | this[LOAD_BOOT_HOOK]('agent'); 17 | (this as any).lifecycle.triggerConfigWillLoad() 18 | }, 19 | 20 | // FIXME: no logger used after egg removed 21 | loadBootHook () { 22 | // do nothing 23 | }, 24 | 25 | [LOAD_BOOT_HOOK] (fileName) { 26 | (this as any).timing.start(`Load ${fileName}.js`) 27 | for (const unit of (this as any).getLoadUnits()) { 28 | const bootFilePath = (this as any).resolveModule(path.join(unit.path, fileName)) 29 | if (!bootFilePath) { 30 | continue 31 | } 32 | const bootHook = (this as any).requireFile(bootFilePath) 33 | if (is.class(bootHook)) { 34 | bootHook.prototype.fullPath = bootFilePath; 35 | // if is boot class, add to lifecycle 36 | (this as any).lifecycle.addBootHook(bootHook) 37 | } else if (is.function(bootHook)) { 38 | // if is boot function, wrap to class 39 | // for compatibility 40 | (this as any).lifecycle.addFunctionAsBootHook(bootHook) 41 | } else { 42 | (this as any).options.logger.warn('[egg-loader] %s must exports a boot class', bootFilePath) 43 | } 44 | } 45 | // init boots 46 | (this as any).lifecycle.init(); 47 | (this as any).timing.end(`Load ${fileName}.js`) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tools/issue-readme/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const { requestPromise } = require('../request-promise') 3 | const issueUrl = 'https://api.github.com/repos/luoxue-victor/learn-node/issues' 4 | const issuesSortByLabel = {} 5 | const path = require('path') 6 | const configPath = path.join(__dirname, 'config.json') 7 | 8 | const time = Date.now() 9 | 10 | const readConfig = JSON.parse(fs.readFileSync(configPath).toString()) 11 | 12 | const writeConfig = (ctx) => { 13 | fs.writeFileSync(configPath, JSON.stringify({ time, ctx })) 14 | } 15 | 16 | let isOverOneDay = false 17 | 18 | if (time - readConfig.time > 1000 * 60 * 60 * 24) isOverOneDay = true 19 | 20 | ;(async () => { 21 | let page = 0 22 | let ctx = '' 23 | const allIssues = [] 24 | 25 | // eslint-disable-next-line no-unmodified-loop-condition 26 | while (isOverOneDay) { 27 | page++ 28 | try { 29 | const issueBody = await requestPromise(issueUrl, { page, state: 'all' }) 30 | console.log(issueBody.length) 31 | if (!issueBody.length) break 32 | allIssues.push(...issueBody) 33 | } catch (error) { 34 | isOverOneDay = false 35 | return console.error(error) 36 | } 37 | } 38 | 39 | allIssues.reverse().forEach(issue => { 40 | issue.labels.forEach(label => { 41 | const labelNmae = label.name 42 | const hasLabel = !!issuesSortByLabel[labelNmae] 43 | if (hasLabel) { 44 | issuesSortByLabel[labelNmae].push(issue) 45 | } else { 46 | issuesSortByLabel[labelNmae] = [issue] 47 | } 48 | }) 49 | }) 50 | 51 | Object.keys(issuesSortByLabel).forEach(name => { 52 | ctx += `\n## ${name} \n\n` 53 | issuesSortByLabel[name].forEach(issue => { 54 | ctx += `- [${issue.title}](${issue.html_url}) \n` 55 | }) 56 | }) 57 | const readme = fs.readFileSync('./README.md').toString() 58 | 59 | isOverOneDay && ctx && writeConfig(ctx) 60 | 61 | fs.writeFileSync('./README.md', readme + (ctx || readConfig.ctx)) 62 | })() 63 | -------------------------------------------------------------------------------- /packages/core/lib/utils/sequencify.ts: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('egg-core#sequencify') 2 | 3 | function sequence (tasks, names, results, missing, recursive, nest, optional, parent) { 4 | names.forEach((name) => { 5 | if (results.requires[name]) return 6 | 7 | const node = tasks[name] 8 | 9 | if (!node) { 10 | if (optional === true) return 11 | missing.push(name) 12 | } else if (nest.includes(name)) { 13 | nest.push(name) 14 | recursive.push(nest.slice(0)) 15 | nest.pop(name) 16 | } else if (node.dependencies.length || node.optionalDependencies.length) { 17 | nest.push(name) 18 | if (node.dependencies.length) { 19 | sequence(tasks, node.dependencies, results, missing, recursive, nest, optional, name) 20 | } 21 | if (node.optionalDependencies.length) { 22 | sequence(tasks, node.optionalDependencies, results, missing, recursive, nest, true, name) 23 | } 24 | nest.pop(name) 25 | } 26 | if (!optional) { 27 | results.requires[name] = true 28 | debug('task: %s is enabled by %s', name, parent) 29 | } 30 | if (!results.sequence.includes(name)) { 31 | results.sequence.push(name) 32 | } 33 | }) 34 | } 35 | 36 | // tasks: object with keys as task names 37 | // names: array of task names 38 | export default (tasks, names) => { 39 | const results = { 40 | sequence: [], 41 | requires: {} 42 | } // the final sequence 43 | const missing = [] // missing tasks 44 | const recursive = [] // recursive task dependencies 45 | 46 | sequence(tasks, names, results, missing, recursive, [], false, 'app') 47 | 48 | if (missing.length || recursive.length) { 49 | results.sequence = [] // results are incomplete at best, completely wrong at worst, remove them to avoid confusion 50 | } 51 | 52 | return { 53 | sequence: results.sequence.filter(item => results.requires[item]), 54 | missingTasks: missing, 55 | recursiveDependencies: recursive 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/core/lib/loader/mixin/custom_loader.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import path from 'path' 3 | const is = require('is-type-of') 4 | 5 | export default { 6 | loadCustomLoader () { 7 | const _that = this as any 8 | assert(_that.config, 'should loadConfig first') 9 | const customLoader = _that.config.customLoader || {} 10 | 11 | for (const property of Object.keys(customLoader)) { 12 | const loaderConfig = Object.assign({}, customLoader[property]) 13 | assert(loaderConfig.directory, `directory is required for config.customLoader.${property}`) 14 | 15 | let directory 16 | if (loaderConfig.loadunit === true) { 17 | directory = (this as any).getLoadUnits().map(unit => path.join(unit.path, loaderConfig.directory)) 18 | } else { 19 | directory = path.join(_that.appInfo.baseDir, loaderConfig.directory) 20 | } 21 | // don't override directory 22 | delete loaderConfig.directory 23 | 24 | const inject = loaderConfig.inject || 'app' 25 | // don't override inject 26 | delete loaderConfig.inject 27 | 28 | switch (inject) { 29 | case 'ctx': { 30 | assert(!(property in _that.app.context), `customLoader should not override ctx.${property}`) 31 | const defaultConfig = { 32 | caseStyle: 'lower', 33 | fieldClass: `${property}Classes` 34 | } 35 | _that.loadToContext(directory, property, Object.assign(defaultConfig, loaderConfig)) 36 | break 37 | } 38 | case 'app': { 39 | assert(!(property in _that.app), `customLoader should not override app.${property}`) 40 | const defaultConfig = { 41 | caseStyle: 'lower', 42 | initializer (Clz) { 43 | return is.class(Clz) ? new Clz(_that.app) : Clz 44 | } 45 | } 46 | _that.loadToApp(directory, property, Object.assign(defaultConfig, loaderConfig)) 47 | break 48 | } 49 | default: 50 | throw new Error('inject only support app or ctx') 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learn-node", 3 | "version": "1.0.0", 4 | "description": "从零开始系统学习node", 5 | "main": "index.js", 6 | "scripts": { 7 | "issue": "node tools/issue-readme", 8 | "log": "conventional-changelog --config ./node_modules/vue-cli-plugin-commitlint/lib/log -i CHANGELOG.md -s -r 0", 9 | "docs": "node tools/create-readme", 10 | "cz": "npm run build && git add . && git cz && git push", 11 | "fix": "webpack-box eslint", 12 | "build": "npm run docs && npm run issue && npm run log", 13 | "start": "nodemon --ext js,ts --exec ts-node app/index.ts", 14 | "demo-koa": "nodemon --ext js,ts --exec ts-node demo/koa/run.ts", 15 | "demo-koa-router": "nodemon --ext js,ts --exec ts-node demo/koa-router/run.ts", 16 | "debug": "nodemon --ext js,ts --exec node -r ts-node/register --inspect app/run.ts" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/luoxue-victor/learn-node.git" 21 | }, 22 | "config": { 23 | "commitizen": { 24 | "path": "./node_modules/vue-cli-plugin-commitlint/lib/cz" 25 | } 26 | }, 27 | "author": "", 28 | "bugs": { 29 | "url": "https://github.com/luoxue-victor/learn-node/issues" 30 | }, 31 | "homepage": "https://github.com/luoxue-victor/learn-node#readme", 32 | "husky": { 33 | "hooks": { 34 | "pre-commit": "lint-staged", 35 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 36 | } 37 | }, 38 | "license": "ISC", 39 | "lint-staged": { 40 | "*.{js,jsx,ts,tsx}": [ 41 | "webpack-box eslint", 42 | "git add" 43 | ] 44 | }, 45 | "devDependencies": { 46 | "@commitlint/config-conventional": "^8.2.0", 47 | "@pkb/cli": "^1.2.10", 48 | "@pkb/plugin-eslint": "^1.2.10", 49 | "@pkb/webpack-box": "^1.2.13", 50 | "@types/koa": "^2.11.3", 51 | "@types/koa-router": "^7.4.0", 52 | "@types/node": "^13.13.2", 53 | "@typescript-eslint/parser": "^2.29.0", 54 | "commitizen": "^4.0.3", 55 | "commitlint": "^8.2.0", 56 | "compatible-require": "^1.0.3", 57 | "conventional-changelog-cli": "^2.0.28", 58 | "husky": "^3.1.0", 59 | "koa": "^2.11.0", 60 | "koa-jwt": "^3.6.0", 61 | "koa-router": "^8.0.8", 62 | "lint-staged": "^9.5.0", 63 | "nodemon": "^2.0.3", 64 | "request": "^2.88.2", 65 | "ts-node": "^8.9.0", 66 | "vue-cli-plugin-commitlint": "^1.0.10" 67 | }, 68 | "dependencies": { 69 | "extend2": "^1.0.0", 70 | "http-errors": "^1.7.3", 71 | "is-type-of": "^1.2.1", 72 | "methods": "^1.1.2", 73 | "path-to-regexp": "^6.1.0", 74 | "ts-events": "^3.4.0", 75 | "urijs": "^1.19.2", 76 | "winston": "^3.2.1" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/core/lib/loader/context_loader.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | const is = require('is-type-of') 3 | const FileLoader = require('./file_loader') 4 | const CLASSLOADER = Symbol('classLoader') 5 | const EXPORTS = FileLoader.EXPORTS 6 | 7 | class ClassLoader { 8 | _cache: Map 9 | _ctx: any 10 | constructor (options) { 11 | assert(options.ctx, 'options.ctx is required') 12 | const properties = options.properties 13 | this._cache = new Map() 14 | this._ctx = options.ctx 15 | 16 | for (const property in properties) { 17 | this.defineProperty(property, properties[property]) 18 | } 19 | } 20 | 21 | defineProperty (property, values) { 22 | Object.defineProperty(this, property, { 23 | get () { 24 | let instance = this._cache.get(property) 25 | if (!instance) { 26 | instance = getInstance(values, this._ctx) 27 | this._cache.set(property, instance) 28 | } 29 | return instance 30 | } 31 | }) 32 | } 33 | } 34 | 35 | export default class ContextLoader extends FileLoader { 36 | constructor (options) { 37 | assert(options.property, 'options.property is required') 38 | assert(options.inject, 'options.inject is required') 39 | const target = options.target = {} 40 | if (options.fieldClass) { 41 | options.inject[options.fieldClass] = target 42 | } 43 | super(options) 44 | 45 | const app = this.options.inject 46 | const property = options.property 47 | 48 | // define ctx.service 49 | Object.defineProperty(app.context, property, { 50 | get () { 51 | // distinguish property cache, 52 | // cache's lifecycle is the same with this context instance 53 | // e.x. ctx.service1 and ctx.service2 have different cache 54 | if (!this[CLASSLOADER]) { 55 | this[CLASSLOADER] = new Map() 56 | } 57 | const classLoader = this[CLASSLOADER] 58 | 59 | let instance = classLoader.get(property) 60 | if (!instance) { 61 | instance = getInstance(target, this) 62 | classLoader.set(property, instance) 63 | } 64 | return instance 65 | } 66 | }) 67 | } 68 | } 69 | 70 | function getInstance (values, ctx) { 71 | // it's a directory when it has no exports 72 | // then use ClassLoader 73 | const Class = values[EXPORTS] ? values : null 74 | let instance 75 | if (Class) { 76 | if (is.class(Class)) { 77 | instance = new Class(ctx) 78 | } else { 79 | // it's just an object 80 | instance = Class 81 | } 82 | // Can't set property to primitive, so check again 83 | // e.x. module.exports = 1; 84 | } else if (is.primitive(values)) { 85 | instance = values 86 | } else { 87 | instance = new ClassLoader({ ctx, properties: values }) 88 | } 89 | return instance 90 | } 91 | -------------------------------------------------------------------------------- /packages/core/lib/utils/index.ts: -------------------------------------------------------------------------------- 1 | const is = require('is-type-of') 2 | const path = require('path') 3 | const fs = require('fs') 4 | const co = require('co') 5 | const BuiltinModule = require('module') 6 | 7 | // Guard against poorly mocked module constructors. 8 | const Module = module.constructor.length > 1 9 | ? module.constructor 10 | /* istanbul ignore next */ 11 | : BuiltinModule 12 | 13 | export default { 14 | extensions: Module._extensions, 15 | 16 | loadFile (filepath) { 17 | try { 18 | // if not js module, just return content buffer 19 | const extname = path.extname(filepath) 20 | if (extname && !Module._extensions[extname]) { 21 | return fs.readFileSync(filepath) 22 | } 23 | // require js module 24 | const obj = require(filepath) 25 | if (!obj) return obj 26 | // it's es module 27 | if (obj.__esModule) return 'default' in obj ? obj.default : obj 28 | return obj 29 | } catch (err) { 30 | err.message = `[egg-core] load file: ${filepath}, error: ${err.message}` 31 | throw err 32 | } 33 | }, 34 | 35 | methods: ['head', 'options', 'get', 'put', 'patch', 'post', 'delete'], 36 | 37 | async callFn (fn, args, ctx) { 38 | args = args || [] 39 | if (!is.function(fn)) return 40 | if (is.generatorFunction(fn)) fn = co.wrap(fn) 41 | return ctx ? fn.call(ctx, ...args) : fn(...args) 42 | }, 43 | 44 | middleware (fn) { 45 | return fn 46 | }, 47 | 48 | getCalleeFromStack (withLine, stackIndex) { 49 | stackIndex = stackIndex === undefined ? 2 : stackIndex 50 | const limit = Error.stackTraceLimit 51 | const prep = Error.prepareStackTrace 52 | 53 | Error.prepareStackTrace = prepareObjectStackTrace 54 | Error.stackTraceLimit = 5 55 | 56 | // capture the stack 57 | const obj = { 58 | stack: [] 59 | } 60 | Error.captureStackTrace(obj) 61 | let callSite = obj.stack[stackIndex] 62 | let fileName 63 | /* istanbul ignore else */ 64 | if (callSite) { 65 | // egg-mock will create a proxy 66 | // https://github.com/eggjs/egg-mock/blob/master/lib/app.js#L174 67 | fileName = callSite.getFileName() 68 | /* istanbul ignore if */ 69 | if (fileName && fileName.endsWith('egg-mock/lib/app.js')) { 70 | // TODO: add test 71 | callSite = obj.stack[stackIndex + 1] 72 | fileName = callSite.getFileName() 73 | } 74 | } 75 | 76 | Error.prepareStackTrace = prep 77 | Error.stackTraceLimit = limit 78 | 79 | /* istanbul ignore if */ 80 | if (!callSite || !fileName) return '' 81 | if (!withLine) return fileName 82 | return `${fileName}:${callSite.getLineNumber()}:${callSite.getColumnNumber()}` 83 | }, 84 | 85 | getResolvedFilename (filepath, baseDir) { 86 | const reg = /[/\\]/g 87 | return filepath.replace(baseDir + path.sep, '').replace(reg, '/') 88 | } 89 | } 90 | 91 | /** 92 | * Capture call site stack from v8. 93 | * https://github.com/v8/v8/wiki/Stack-Trace-API 94 | */ 95 | 96 | function prepareObjectStackTrace (obj, stack) { 97 | return stack 98 | } 99 | -------------------------------------------------------------------------------- /tools/issue-readme/config.json: -------------------------------------------------------------------------------- 1 | {"time":1588758857336,"ctx":"\n## DONE \n\n- [第一课:项目脚手架](https://github.com/luoxue-victor/learn-node/issues/1) \n\n## TODO \n\n- [手写koa/koa-router源码](https://github.com/luoxue-victor/learn-node/issues/2) \n- [WebSocket](https://github.com/luoxue-victor/learn-node/issues/3) \n- [nginx](https://github.com/luoxue-victor/learn-node/issues/4) \n- [爬虫](https://github.com/luoxue-victor/learn-node/issues/5) \n- [连接数据库](https://github.com/luoxue-victor/learn-node/issues/6) \n- [发送邮件](https://github.com/luoxue-victor/learn-node/issues/7) \n- [graphql](https://github.com/luoxue-victor/learn-node/issues/8) \n- [SSR](https://github.com/luoxue-victor/learn-node/issues/9) \n- [微服务与RPC](https://github.com/luoxue-victor/learn-node/issues/10) \n- [docker](https://github.com/luoxue-victor/learn-node/issues/11) \n- [serverless](https://github.com/luoxue-victor/learn-node/issues/12) \n- [nodejs高可用](https://github.com/luoxue-victor/learn-node/issues/13) \n- [mongo模糊查询](https://github.com/luoxue-victor/learn-node/issues/14) \n- [日志管理 log4js](https://github.com/luoxue-victor/learn-node/issues/15) \n- [Nginx反向代理Nodejs – log4js日志IP显示错误](https://github.com/luoxue-victor/learn-node/issues/16) \n- [Yeoman自动构建js项目](https://github.com/luoxue-victor/learn-node/issues/17) \n- [CNPM搭建私有的NPM服务](https://github.com/luoxue-victor/learn-node/issues/18) \n- [单元测试](https://github.com/luoxue-victor/learn-node/issues/19) \n- [登陆系统](https://github.com/luoxue-victor/learn-node/issues/20) \n- [工具](https://github.com/luoxue-victor/learn-node/issues/21) \n- [TS搭建](https://github.com/luoxue-victor/learn-node/issues/22) \n- [缓存](https://github.com/luoxue-victor/learn-node/issues/23) \n- [k8s](https://github.com/luoxue-victor/learn-node/issues/24) \n- [分布式](https://github.com/luoxue-victor/learn-node/issues/25) \n- [redis集群](https://github.com/luoxue-victor/learn-node/issues/26) \n- [熔断机制](https://github.com/luoxue-victor/learn-node/issues/27) \n- [负载均衡](https://github.com/luoxue-victor/learn-node/issues/28) \n- [设计一个高并发系统](https://github.com/luoxue-victor/learn-node/issues/29) \n- [在 Node 应用中利用多核心CPU的优势](https://github.com/luoxue-victor/learn-node/issues/30) \n- [监控系统](https://github.com/luoxue-victor/learn-node/issues/31) \n- [nodejs 线上常见问题](https://github.com/luoxue-victor/learn-node/issues/32) \n- [node诊断工具](https://github.com/luoxue-victor/learn-node/issues/33) \n- [ProtoBuf](https://github.com/luoxue-victor/learn-node/issues/34) \n- [切割pdf](https://github.com/luoxue-victor/learn-node/issues/35) \n- [thrift idl 自动生成代码](https://github.com/luoxue-victor/learn-node/issues/36) \n- [Node.js + Consul 实现服务注册、健康检查、配置中心](https://github.com/luoxue-victor/learn-node/issues/37) \n- [打通 HTTP 与 RPC](https://github.com/luoxue-victor/learn-node/issues/38) \n- [服务治理](https://github.com/luoxue-victor/learn-node/issues/39) \n- [服务调用遇到的一些问题](https://github.com/luoxue-victor/learn-node/issues/40) \n- [ServiceMesh](https://github.com/luoxue-victor/learn-node/issues/41) \n- [服务发现](https://github.com/luoxue-victor/learn-node/issues/42) \n- [GraphQL 解决方案 Apollo 之 Apollo Client](https://github.com/luoxue-victor/learn-node/issues/43) \n- [Apollo Server](https://github.com/luoxue-victor/learn-node/issues/44) \n- [node 开发 调试 运行](https://github.com/luoxue-victor/learn-node/issues/45) \n- [CI/CD/CD - 持续集成/持续交付/持续部署](https://github.com/luoxue-victor/learn-node/issues/46) \n- [分布式消息队列](https://github.com/luoxue-victor/learn-node/issues/47) \n"} -------------------------------------------------------------------------------- /demo/koa-router/Layer.ts: -------------------------------------------------------------------------------- 1 | import { pathToRegexp, compile, parse } from 'path-to-regexp' 2 | import uri from 'urijs' 3 | 4 | const debug = require('debug')('koa-router') 5 | 6 | export default function Layer (this: any, path, methods, middleware, opts) { 7 | this.opts = opts || {} 8 | this.name = this.opts.name || null 9 | this.methods = [] 10 | this.paramNames = [] 11 | this.stack = Array.isArray(middleware) ? middleware : [middleware] 12 | 13 | methods.forEach(function (this: any, method) { 14 | const l = this.methods.push(method.toUpperCase()) 15 | if (this.methods[l - 1] === 'GET') { 16 | this.methods.unshift('HEAD') 17 | } 18 | }, this) 19 | 20 | this.path = path 21 | this.regexp = pathToRegexp(path, this.paramNames, this.opts) 22 | 23 | debug('defined route %s %s', this.methods, this.opts.prefix + this.path) 24 | } 25 | 26 | Layer.prototype.match = function (path) { 27 | return this.regexp.test(path) 28 | } 29 | 30 | Layer.prototype.params = function (path, captures, existingParams) { 31 | var params = existingParams || {} 32 | 33 | for (var len = captures.length, i = 0; i < len; i++) { 34 | if (this.paramNames[i]) { 35 | var c = captures[i] 36 | params[this.paramNames[i].name] = c ? safeDecodeURIComponent(c) : c 37 | } 38 | } 39 | 40 | return params 41 | } 42 | 43 | Layer.prototype.captures = function (path) { 44 | if (this.opts.ignoreCaptures) return [] 45 | return path.match(this.regexp).slice(1) 46 | } 47 | 48 | Layer.prototype.url = function (params, options) { 49 | let args = params 50 | const url = this.path.replace(/\(\.\*\)/g, '') 51 | const toPath = compile(url) 52 | let replaced 53 | 54 | if (typeof params !== 'object') { 55 | args = Array.prototype.slice.call(arguments) 56 | if (typeof args[args.length - 1] === 'object') { 57 | options = args[args.length - 1] 58 | args = args.slice(0, args.length - 1) 59 | } 60 | } 61 | 62 | const tokens = parse(url) 63 | let replace = {} 64 | 65 | if (args instanceof Array) { 66 | for (let len = tokens.length, i = 0, j = 0; i < len; i++) { 67 | if ((tokens[i] as any).name) replace[(tokens[i] as any).name] = args[j++] 68 | } 69 | } else if (tokens.some((token) => (token as any).name)) { 70 | replace = params 71 | } else { 72 | options = params 73 | } 74 | 75 | replaced = toPath(replace) 76 | 77 | if (options && options.query) { 78 | replaced = new uri(replaced) 79 | replaced.search(options.query) 80 | return replaced.toString() 81 | } 82 | 83 | return replaced 84 | } 85 | 86 | Layer.prototype.param = function (param, fn) { 87 | var stack = this.stack 88 | var params = this.paramNames 89 | var middleware = function (this: any, ctx, next) { 90 | return fn.call(this, ctx.params[param], ctx, next) 91 | } 92 | 93 | ;(middleware as any).param = param 94 | 95 | var names = params.map(function (p) { 96 | return p.name 97 | }) 98 | 99 | var x = names.indexOf(param) 100 | if (x > -1) { 101 | stack.some(function (fn, i) { 102 | if (!fn.param || names.indexOf(fn.param) > x) { 103 | stack.splice(i, 0, middleware) 104 | return true 105 | } 106 | }) 107 | } 108 | 109 | return this 110 | } 111 | 112 | Layer.prototype.setPrefix = function (prefix) { 113 | if (this.path) { 114 | this.path = prefix + this.path 115 | this.paramNames = [] 116 | this.regexp = pathToRegexp(this.path, this.paramNames, this.opts) 117 | } 118 | 119 | return this 120 | } 121 | 122 | function safeDecodeURIComponent (text) { 123 | try { 124 | return decodeURIComponent(text) 125 | } catch (e) { 126 | return text 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.0 (2020-05-06) 2 | 3 | ### 🌟 新功能 4 | 范围|描述|commitId 5 | --|--|-- 6 | app | 增加一些基础的koa封装 | [d06a6ab](https://github.com/luoxue-victor/learn-node/commit/d06a6ab) 7 | core | 部分改成ts | [0824d08](https://github.com/luoxue-victor/learn-node/commit/0824d08) 8 | core | 脚手架核心功能初次提交 | [b33ad8d](https://github.com/luoxue-victor/learn-node/commit/b33ad8d) 9 | core | 所有文件转换成ts | [c2149a3](https://github.com/luoxue-victor/learn-node/commit/c2149a3) 10 | core | mixin 转成 ts | [c388c58](https://github.com/luoxue-victor/learn-node/commit/c388c58) 11 | demo | koa完成 | [df5c86d](https://github.com/luoxue-victor/learn-node/commit/df5c86d) 12 | issue | 修改issue获取接口 | [0e0109a](https://github.com/luoxue-victor/learn-node/commit/0e0109a) 13 | koa | 引入koa | [1075566](https://github.com/luoxue-victor/learn-node/commit/1075566) 14 | qwe | qweq | [33ed50f](https://github.com/luoxue-victor/learn-node/commit/33ed50f) 15 | test | test | [afcf94b](https://github.com/luoxue-victor/learn-node/commit/afcf94b) 16 | test | test | [def2c21](https://github.com/luoxue-victor/learn-node/commit/def2c21) 17 | test | test | [252be4f](https://github.com/luoxue-victor/learn-node/commit/252be4f) 18 | test | test | [32c1f6d](https://github.com/luoxue-victor/learn-node/commit/32c1f6d) 19 | 20 | 21 | ### 🐛 Bug 修复 22 | 范围|描述|commitId 23 | --|--|-- 24 | package.json | 修改cz命令 | [09b1079](https://github.com/luoxue-victor/learn-node/commit/09b1079) 25 | src | test | [c962c93](https://github.com/luoxue-victor/learn-node/commit/c962c93) 26 | test | test | [6e82193](https://github.com/luoxue-victor/learn-node/commit/6e82193) 27 | test | test | [0c2e4eb](https://github.com/luoxue-victor/learn-node/commit/0c2e4eb) 28 | test | test | [6928fa4](https://github.com/luoxue-victor/learn-node/commit/6928fa4) 29 | test | test | [c7dc326](https://github.com/luoxue-victor/learn-node/commit/c7dc326) 30 | test | test | [c4316e6](https://github.com/luoxue-victor/learn-node/commit/c4316e6) 31 | test | test | [94a1e02](https://github.com/luoxue-victor/learn-node/commit/94a1e02) 32 | 33 | 34 | ### 📝 文档 35 | 范围|描述|commitId 36 | --|--|-- 37 | docs | 修改md | [38d175b](https://github.com/luoxue-victor/learn-node/commit/38d175b) 38 | docs | 自动生成文档 | [796a9a8](https://github.com/luoxue-victor/learn-node/commit/796a9a8) 39 | readme | 跟新readme | [a825983](https://github.com/luoxue-victor/learn-node/commit/a825983) 40 | readme | 生成readme | [163edeb](https://github.com/luoxue-victor/learn-node/commit/163edeb) 41 | 42 | 43 | ### 📦 持续集成 44 | 范围|描述|commitId 45 | --|--|-- 46 | all | 构建项目 | [dc049d1](https://github.com/luoxue-victor/learn-node/commit/dc049d1) 47 | 48 | 49 | ### 🔧 测试 50 | 范围|描述|commitId 51 | --|--|-- 52 | src | 测试eslint | [5280358](https://github.com/luoxue-victor/learn-node/commit/5280358) 53 | 54 | 55 | ### chore 56 | 范围|描述|commitId 57 | --|--|-- 58 | editor | 编译器配置 | [991d891](https://github.com/luoxue-victor/learn-node/commit/991d891) 59 | eslint | 支持可选链 | [8d46567](https://github.com/luoxue-victor/learn-node/commit/8d46567) 60 | issue | 修改issue生成 | [82920d3](https://github.com/luoxue-victor/learn-node/commit/82920d3) 61 | prettier | 项目配置优化 | [22cc54b](https://github.com/luoxue-victor/learn-node/commit/22cc54b) 62 | tools | 自动生成readme | [59346c7](https://github.com/luoxue-victor/learn-node/commit/59346c7) 63 | ts | 搭建ts | [757960f](https://github.com/luoxue-victor/learn-node/commit/757960f) 64 | 65 | 66 | 范围|描述|commitId 67 | --|--|-- 68 | - | Update README.md | [3529385](https://github.com/luoxue-victor/learn-node/commit/3529385) 69 | - | Update README.md | [ebe4f02](https://github.com/luoxue-victor/learn-node/commit/ebe4f02) 70 | - | Update README.md | [0557329](https://github.com/luoxue-victor/learn-node/commit/0557329) 71 | - | Update README.md | [b715a6e](https://github.com/luoxue-victor/learn-node/commit/b715a6e) 72 | - | Initial commit | [61d56a5](https://github.com/luoxue-victor/learn-node/commit/61d56a5) 73 | 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 学习 node 开始 2 | 3 | ## 所有课题 4 |
5 | 点击关闭/打开所有课题 6 |
7 | 8 | 9 | - [面试题](./docs/00-node面试题.md) 10 | - [搭建项目基础](./docs/01-搭建项目基础.md) 11 | - [Koa 搭建](./docs/02-封装koa|koa-router.md) 12 | 13 |
14 | 15 | ## 安装 16 | 17 | ```bash 18 | git clone https://github.com/luoxue-victor/learn-node.git 19 | cd learn-node 20 | yarn install 21 | ``` 22 | 23 | 24 | ## DONE 25 | 26 | - [第一课:项目脚手架](https://github.com/luoxue-victor/learn-node/issues/1) 27 | 28 | ## TODO 29 | 30 | - [手写koa/koa-router源码](https://github.com/luoxue-victor/learn-node/issues/2) 31 | - [WebSocket](https://github.com/luoxue-victor/learn-node/issues/3) 32 | - [nginx](https://github.com/luoxue-victor/learn-node/issues/4) 33 | - [爬虫](https://github.com/luoxue-victor/learn-node/issues/5) 34 | - [连接数据库](https://github.com/luoxue-victor/learn-node/issues/6) 35 | - [发送邮件](https://github.com/luoxue-victor/learn-node/issues/7) 36 | - [graphql](https://github.com/luoxue-victor/learn-node/issues/8) 37 | - [SSR](https://github.com/luoxue-victor/learn-node/issues/9) 38 | - [微服务与RPC](https://github.com/luoxue-victor/learn-node/issues/10) 39 | - [docker](https://github.com/luoxue-victor/learn-node/issues/11) 40 | - [serverless](https://github.com/luoxue-victor/learn-node/issues/12) 41 | - [nodejs高可用](https://github.com/luoxue-victor/learn-node/issues/13) 42 | - [mongo模糊查询](https://github.com/luoxue-victor/learn-node/issues/14) 43 | - [日志管理 log4js](https://github.com/luoxue-victor/learn-node/issues/15) 44 | - [Nginx反向代理Nodejs – log4js日志IP显示错误](https://github.com/luoxue-victor/learn-node/issues/16) 45 | - [Yeoman自动构建js项目](https://github.com/luoxue-victor/learn-node/issues/17) 46 | - [CNPM搭建私有的NPM服务](https://github.com/luoxue-victor/learn-node/issues/18) 47 | - [单元测试](https://github.com/luoxue-victor/learn-node/issues/19) 48 | - [登陆系统](https://github.com/luoxue-victor/learn-node/issues/20) 49 | - [工具](https://github.com/luoxue-victor/learn-node/issues/21) 50 | - [TS搭建](https://github.com/luoxue-victor/learn-node/issues/22) 51 | - [缓存](https://github.com/luoxue-victor/learn-node/issues/23) 52 | - [k8s](https://github.com/luoxue-victor/learn-node/issues/24) 53 | - [分布式](https://github.com/luoxue-victor/learn-node/issues/25) 54 | - [redis集群](https://github.com/luoxue-victor/learn-node/issues/26) 55 | - [熔断机制](https://github.com/luoxue-victor/learn-node/issues/27) 56 | - [负载均衡](https://github.com/luoxue-victor/learn-node/issues/28) 57 | - [设计一个高并发系统](https://github.com/luoxue-victor/learn-node/issues/29) 58 | - [在 Node 应用中利用多核心CPU的优势](https://github.com/luoxue-victor/learn-node/issues/30) 59 | - [监控系统](https://github.com/luoxue-victor/learn-node/issues/31) 60 | - [nodejs 线上常见问题](https://github.com/luoxue-victor/learn-node/issues/32) 61 | - [node诊断工具](https://github.com/luoxue-victor/learn-node/issues/33) 62 | - [ProtoBuf](https://github.com/luoxue-victor/learn-node/issues/34) 63 | - [切割pdf](https://github.com/luoxue-victor/learn-node/issues/35) 64 | - [thrift idl 自动生成代码](https://github.com/luoxue-victor/learn-node/issues/36) 65 | - [Node.js + Consul 实现服务注册、健康检查、配置中心](https://github.com/luoxue-victor/learn-node/issues/37) 66 | - [打通 HTTP 与 RPC](https://github.com/luoxue-victor/learn-node/issues/38) 67 | - [服务治理](https://github.com/luoxue-victor/learn-node/issues/39) 68 | - [服务调用遇到的一些问题](https://github.com/luoxue-victor/learn-node/issues/40) 69 | - [ServiceMesh](https://github.com/luoxue-victor/learn-node/issues/41) 70 | - [服务发现](https://github.com/luoxue-victor/learn-node/issues/42) 71 | - [GraphQL 解决方案 Apollo 之 Apollo Client](https://github.com/luoxue-victor/learn-node/issues/43) 72 | - [Apollo Server](https://github.com/luoxue-victor/learn-node/issues/44) 73 | - [node 开发 调试 运行](https://github.com/luoxue-victor/learn-node/issues/45) 74 | - [CI/CD/CD - 持续集成/持续交付/持续部署](https://github.com/luoxue-victor/learn-node/issues/46) 75 | - [分布式消息队列](https://github.com/luoxue-victor/learn-node/issues/47) 76 | -------------------------------------------------------------------------------- /packages/core/lib/loader/mixin/config.ts: -------------------------------------------------------------------------------- 1 | import { Console } from 'console' 2 | import assert from 'assert' 3 | import path from 'path' 4 | const debug = require('debug')('egg-core:config') 5 | 6 | const extend = require('extend2') 7 | 8 | const SET_CONFIG_META = Symbol('Loader#setConfigMeta') 9 | 10 | module.exports = { 11 | 12 | loadConfig () { 13 | this.timing.start('Load Config') 14 | this.configMeta = {} 15 | 16 | const target = { 17 | coreMiddleware: null, 18 | coreMiddlewares: null, 19 | appMiddleware: null, 20 | appMiddlewares: null, 21 | middleware: null 22 | } 23 | 24 | // Load Application config first 25 | const appConfig = this._preloadAppConfig() 26 | 27 | // plugin config.default 28 | // framework config.default 29 | // app config.default 30 | // plugin config.{env} 31 | // framework config.{env} 32 | // app config.{env} 33 | for (const filename of this.getTypeFiles('config')) { 34 | for (const unit of this.getLoadUnits()) { 35 | const isApp = unit.type === 'app' 36 | const config = this._loadConfig(unit.path, filename, isApp ? undefined : appConfig, unit.type) 37 | 38 | if (!config) { 39 | continue 40 | } 41 | 42 | debug('Loaded config %s/%s, %j', unit.path, filename, config) 43 | extend(true, target, config) 44 | } 45 | } 46 | 47 | // You can manipulate the order of app.config.coreMiddleware and app.config.appMiddleware in app.js 48 | target.coreMiddleware = target.coreMiddlewares = target.coreMiddleware || [] 49 | target.appMiddleware = target.appMiddlewares = target.middleware || [] 50 | 51 | this.config = target 52 | this.timing.end('Load Config') 53 | }, 54 | 55 | _preloadAppConfig () { 56 | const names = [ 57 | 'config.default', 58 | `config.${this.serverEnv}` 59 | ] 60 | const target = {} 61 | for (const filename of names) { 62 | const config = this._loadConfig(this.options.baseDir, filename, undefined, 'app') 63 | extend(true, target, config) 64 | } 65 | return target 66 | }, 67 | 68 | _loadConfig (dirpath, filename, extraInject, type) { 69 | const isPlugin = type === 'plugin' 70 | const isApp = type === 'app' 71 | 72 | let filepath = this.resolveModule(path.join(dirpath, 'config', filename)) 73 | // let config.js compatible 74 | if (filename === 'config.default' && !filepath) { 75 | filepath = this.resolveModule(path.join(dirpath, 'config/config')) 76 | } 77 | const config = this.loadFile(filepath, this.appInfo, extraInject) 78 | 79 | if (!config) return null 80 | 81 | if (isPlugin || isApp) { 82 | assert(!config.coreMiddleware, 'Can not define coreMiddleware in app or plugin') 83 | } 84 | if (!isApp) { 85 | assert(!config.middleware, 'Can not define middleware in ' + filepath) 86 | } 87 | 88 | // store config meta, check where is the property of config come from. 89 | this[SET_CONFIG_META](config, filepath) 90 | 91 | return config 92 | }, 93 | 94 | [SET_CONFIG_META] (config, filepath) { 95 | config = extend(true, {}, config) 96 | setConfig(config, filepath) 97 | extend(true, this.configMeta, config) 98 | } 99 | } 100 | 101 | function setConfig (obj, filepath) { 102 | for (const key of Object.keys(obj)) { 103 | const val = obj[key] 104 | // ignore console 105 | if (key === 'console' && val && typeof val.Console === 'function' && val.Console === Console) { 106 | obj[key] = filepath 107 | continue 108 | } 109 | if (val && Object.getPrototypeOf(val) === Object.prototype && Object.keys(val).length > 0) { 110 | setConfig(val, filepath) 111 | continue 112 | } 113 | obj[key] = filepath 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /packages/core/lib/loader/mixin/middleware.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | const join = require('path').join 3 | const is = require('is-type-of') 4 | const inspect = require('util').inspect 5 | const debug = require('debug')('egg-core:middleware') 6 | const pathMatching = require('egg-path-matching') 7 | const utils = require('../../utils') 8 | 9 | export default { 10 | 11 | /** 12 | * Load app/middleware 13 | * 14 | * app.config.xx is the options of the middleware xx that has same name as config 15 | * 16 | * @function EggLoader#loadMiddleware 17 | * @param {Object} opt - LoaderOptions 18 | * @example 19 | * ```js 20 | * // app/middleware/status.js 21 | * module.exports = function(options, app) { 22 | * // options == app.config.status 23 | * return function*(next) { 24 | * yield next; 25 | * } 26 | * } 27 | * ``` 28 | * @since 1.0.0 29 | */ 30 | loadMiddleware (opt) { 31 | const _that = this as any 32 | _that.timing.start('Load Middleware') 33 | const app = _that.app 34 | 35 | // load middleware to app.middleware 36 | opt = Object.assign({ 37 | call: false, 38 | override: true, 39 | caseStyle: 'lower', 40 | directory: _that.getLoadUnits().map(unit => join(unit.path, 'app/middleware')) 41 | }, opt) 42 | const middlewarePaths = opt.directory 43 | _that.loadToApp(middlewarePaths, 'middlewares', opt) 44 | 45 | for (const name in app.middlewares) { 46 | Object.defineProperty(app.middleware, name, { 47 | get () { 48 | return app.middlewares[name] 49 | }, 50 | enumerable: false, 51 | configurable: false 52 | }) 53 | } 54 | 55 | _that.options.logger.info('Use coreMiddleware order: %j', _that.config.coreMiddleware) 56 | _that.options.logger.info('Use appMiddleware order: %j', _that.config.appMiddleware) 57 | 58 | // use middleware ordered by app.config.coreMiddleware and app.config.appMiddleware 59 | const middlewareNames = _that.config.coreMiddleware.concat(_that.config.appMiddleware) 60 | debug('middlewareNames: %j', middlewareNames) 61 | const middlewaresMap = new Map() 62 | for (const name of middlewareNames) { 63 | if (!app.middlewares[name]) { 64 | throw new TypeError(`Middleware ${name} not found`) 65 | } 66 | if (middlewaresMap.has(name)) { 67 | throw new TypeError(`Middleware ${name} redefined`) 68 | } 69 | middlewaresMap.set(name, true) 70 | 71 | const options = _that.config[name] || {} 72 | let mw = app.middlewares[name] 73 | mw = mw(options, app) 74 | assert(is.function(mw), `Middleware ${name} must be a function, but actual is ${inspect(mw)}`) 75 | mw._name = name 76 | // middlewares support options.enable, options.ignore and options.match 77 | mw = wrapMiddleware(mw, options) 78 | if (mw) { 79 | if (debug.enabled) { 80 | // show mw debug log on every request 81 | mw = debugWrapper(mw) 82 | } 83 | app.use(mw) 84 | debug('Use middleware: %s with options: %j', name, options) 85 | _that.options.logger.info('[egg:loader] Use middleware: %s', name) 86 | } else { 87 | _that.options.logger.info('[egg:loader] Disable middleware: %s', name) 88 | } 89 | } 90 | 91 | _that.options.logger.info('[egg:loader] Loaded middleware from %j', middlewarePaths) 92 | _that.timing.end('Load Middleware') 93 | } 94 | 95 | } 96 | 97 | function wrapMiddleware (mw, options) { 98 | // support options.enable 99 | if (options.enable === false) return null 100 | 101 | // support generator function 102 | mw = utils.middleware(mw) 103 | 104 | // support options.match and options.ignore 105 | if (!options.match && !options.ignore) return mw 106 | const match = pathMatching(options) 107 | 108 | const fn = (ctx, next) => { 109 | if (!match(ctx)) return next() 110 | return mw(ctx, next) 111 | } 112 | fn._name = mw._name + 'middlewareWrapper' 113 | return fn 114 | } 115 | 116 | function debugWrapper (mw) { 117 | const fn = (ctx, next) => { 118 | debug('[%s %s] enter middleware: %s', ctx.method, ctx.url, mw._name) 119 | return mw(ctx, next) 120 | } 121 | fn._name = mw._name + 'DebugWrapper' 122 | return fn 123 | } 124 | -------------------------------------------------------------------------------- /packages/core/lib/loader/mixin/controller.ts: -------------------------------------------------------------------------------- 1 | import utils from '../../utils' 2 | import path from 'path' 3 | const is = require('is-type-of') 4 | const utility = require('utility') 5 | const FULLPATH = require('../file_loader').FULLPATH 6 | 7 | export default { 8 | 9 | loadController (opt: { directory: any }) { 10 | (this as any).timing.start('Load Controller') 11 | opt = Object.assign({ 12 | caseStyle: 'lower', 13 | directory: path.join((this as any).options.baseDir, 'app/controller'), 14 | initializer: (obj: { (arg0: any): any; prototype: { pathName: any; fullPath: any } }, opt: { pathName: any; path: any }) => { 15 | if (is.function(obj) && !is.generatorFunction(obj) && !is.class(obj) && !is.asyncFunction(obj)) { 16 | obj = obj((this as any).app) 17 | } 18 | if (is.class(obj)) { 19 | obj.prototype.pathName = opt.pathName 20 | obj.prototype.fullPath = opt.path 21 | return wrapClass(obj) 22 | } 23 | if (is.object(obj)) { 24 | return wrapObject(obj, opt.path) 25 | } 26 | if (is.generatorFunction(obj) || is.asyncFunction(obj)) { 27 | return wrapObject({ 'module.exports': obj }, opt.path)['module.exports'] 28 | } 29 | return obj 30 | } 31 | }, opt) 32 | // eslint-disable-next-line func-call-spacing 33 | const controllerBase: any = opt.directory; 34 | 35 | (this as any).loadToApp(controllerBase, 'controller', opt); 36 | (this as any).options.logger.info('[egg:loader] Controller loaded: %s', controllerBase); 37 | (this as any).timing.end('Load Controller') 38 | } 39 | 40 | } 41 | 42 | // wrap the class, yield a object with middlewares 43 | function wrapClass (Controller: { prototype: { fullPath: string }; name: string }) { 44 | let proto = Controller.prototype 45 | const ret = {} 46 | // tracing the prototype chain 47 | while (proto !== Object.prototype) { 48 | const keys = Object.getOwnPropertyNames(proto) 49 | for (const key of keys) { 50 | // getOwnPropertyNames will return constructor 51 | // that should be ignored 52 | if (key === 'constructor') { 53 | continue 54 | } 55 | // skip getter, setter & non-function properties 56 | const d = Object.getOwnPropertyDescriptor(proto, key) 57 | // prevent to override sub method 58 | // eslint-disable-next-line no-prototype-builtins 59 | if (is.function(d.value) && !ret.hasOwnProperty(key)) { 60 | ret[key] = methodToMiddleware(Controller as any, key) 61 | ret[key][FULLPATH] = Controller.prototype.fullPath + '#' + Controller.name + '.' + key + '()' 62 | } 63 | } 64 | proto = Object.getPrototypeOf(proto) 65 | } 66 | return ret 67 | 68 | function methodToMiddleware (Controller: new (arg0: any) => any, key: string) { 69 | return function classControllerMiddleware (this: any, ...args) { 70 | const controller = new Controller(this) 71 | if (!this.app.config.controller || !this.app.config.controller.supportParams) { 72 | args = [this] 73 | } 74 | return utils.callFn(controller[key], args, controller) 75 | } 76 | } 77 | } 78 | 79 | // wrap the method of the object, method can receive ctx as it's first argument 80 | function wrapObject (obj: { [x: string]: any; 'module.exports'?: any }, path: any, prefix?: string) { 81 | const keys = Object.keys(obj) 82 | const ret = {} 83 | for (const key of keys) { 84 | if (is.function(obj[key])) { 85 | const names = utility.getParamNames(obj[key]) 86 | if (names[0] === 'next') { 87 | throw new Error(`controller \`${prefix || ''}${key}\` should not use next as argument from file ${path}`) 88 | } 89 | ret[key] = functionToMiddleware(obj[key]) 90 | ret[key][FULLPATH] = `${path}#${prefix || ''}${key}()` 91 | } else if (is.object(obj[key])) { 92 | ret[key] = wrapObject(obj[key], path, `${prefix || ''}${key}.`) 93 | } 94 | } 95 | return ret 96 | 97 | function functionToMiddleware (func: { [x: string]: any }) { 98 | const objectControllerMiddleware = async function (this: any, ...args) { 99 | if (!this.app.config.controller || !this.app.config.controller.supportParams) { 100 | args = [this] 101 | } 102 | return await utils.callFn(func, args, this) 103 | } 104 | for (const key in func) { 105 | objectControllerMiddleware[key] = func[key] 106 | } 107 | return objectControllerMiddleware 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /packages/core/lib/loader/mixin/extend.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | const debug = require('debug')('egg-core:extend') 3 | const deprecate = require('depd')('egg') 4 | 5 | const originalPrototypes = { 6 | request: require('koa/lib/request'), 7 | response: require('koa/lib/response'), 8 | context: require('koa/lib/context'), 9 | application: require('koa/lib/application') 10 | } 11 | 12 | export default { 13 | 14 | /** 15 | * mixin Agent.prototype 16 | * @function EggLoader#loadAgentExtend 17 | * @since 1.0.0 18 | */ 19 | loadAgentExtend () { 20 | this.loadExtend('agent', (this as any).app) 21 | }, 22 | 23 | /** 24 | * mixin Application.prototype 25 | * @function EggLoader#loadApplicationExtend 26 | * @since 1.0.0 27 | */ 28 | loadApplicationExtend () { 29 | this.loadExtend('application', (this as any).app) 30 | }, 31 | 32 | /** 33 | * mixin Request.prototype 34 | * @function EggLoader#loadRequestExtend 35 | * @since 1.0.0 36 | */ 37 | loadRequestExtend () { 38 | this.loadExtend('request', (this as any).app.request) 39 | }, 40 | 41 | /** 42 | * mixin Response.prototype 43 | * @function EggLoader#loadResponseExtend 44 | * @since 1.0.0 45 | */ 46 | loadResponseExtend () { 47 | this.loadExtend('response', (this as any).app.response) 48 | }, 49 | 50 | /** 51 | * mixin Context.prototype 52 | * @function EggLoader#loadContextExtend 53 | * @since 1.0.0 54 | */ 55 | loadContextExtend () { 56 | this.loadExtend('context', (this as any).app.context) 57 | }, 58 | 59 | /** 60 | * mixin app.Helper.prototype 61 | * @function EggLoader#loadHelperExtend 62 | * @since 1.0.0 63 | */ 64 | loadHelperExtend () { 65 | if ((this as any).app && (this as any).app.Helper) { 66 | this.loadExtend('helper', (this as any).app.Helper.prototype) 67 | } 68 | }, 69 | 70 | /** 71 | * Find all extend file paths by name 72 | * can be override in top level framework to support load `app/extends/{name}.js` 73 | * 74 | * @param {String} name - filename which may be `app/extend/{name}.js` 75 | * @return {Array} filepaths extend file paths 76 | * @private 77 | */ 78 | getExtendFilePaths (name) { 79 | return (this as any).getLoadUnits().map(unit => path.join(unit.path, 'app/extend', name)) 80 | }, 81 | 82 | /** 83 | * Loader app/extend/xx.js to `prototype`, 84 | * @function loadExtend 85 | * @param {String} name - filename which may be `app/extend/{name}.js` 86 | * @param {Object} proto - prototype that mixed 87 | * @since 1.0.0 88 | */ 89 | loadExtend (name, proto) { 90 | (this as any).timing.start(`Load extend/${name}.js`) 91 | // All extend files 92 | const filepaths = this.getExtendFilePaths(name) 93 | // if use mm.env and serverEnv is not unittest 94 | const isAddUnittest = 'EGG_MOCK_SERVER_ENV' in process.env && (this as any).serverEnv !== 'unittest' 95 | for (let i = 0, l = filepaths.length; i < l; i++) { 96 | const filepath = filepaths[i] 97 | filepaths.push(filepath + `.${(this as any).serverEnv}`) 98 | if (isAddUnittest) filepaths.push(filepath + '.unittest') 99 | } 100 | 101 | const mergeRecord = new Map() 102 | for (let filepath of filepaths) { 103 | filepath = (this as any).resolveModule(filepath) 104 | if (!filepath) { 105 | continue 106 | } else if (filepath.endsWith('/index.js')) { 107 | // TODO: remove support at next version 108 | deprecate(`app/extend/${name}/index.js is deprecated, use app/extend/${name}.js instead`) 109 | } 110 | 111 | const ext = (this as any).requireFile(filepath) 112 | 113 | const properties = Object.getOwnPropertyNames(ext) 114 | .concat(Object.getOwnPropertySymbols(ext) as any) 115 | 116 | for (const property of properties) { 117 | if (mergeRecord.has(property)) { 118 | debug('Property: "%s" already exists in "%s",it will be redefined by "%s"', 119 | property, mergeRecord.get(property), filepath) 120 | } 121 | 122 | // Copy descriptor 123 | let descriptor = Object.getOwnPropertyDescriptor(ext, property) 124 | let originalDescriptor = Object.getOwnPropertyDescriptor(proto, property) 125 | if (!originalDescriptor) { 126 | // try to get descriptor from originalPrototypes 127 | const originalProto = originalPrototypes[name] 128 | if (originalProto) { 129 | originalDescriptor = Object.getOwnPropertyDescriptor(originalProto, property) 130 | } 131 | } 132 | if (originalDescriptor) { 133 | // don't override descriptor 134 | descriptor = Object.assign({}, descriptor) 135 | if (!descriptor.set && originalDescriptor.set) { 136 | descriptor.set = originalDescriptor.set 137 | } 138 | if (!descriptor.get && originalDescriptor.get) { 139 | descriptor.get = originalDescriptor.get 140 | } 141 | } 142 | Object.defineProperty(proto, property, descriptor) 143 | mergeRecord.set(property, filepath) 144 | } 145 | debug('merge %j to %s from %s', Object.keys(ext), name, filepath) 146 | } 147 | (this as any).timing.end(`Load extend/${name}.js`) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /packages/core/lib/egg.ts: -------------------------------------------------------------------------------- 1 | import KoaApplication from 'koa' 2 | 3 | const assert = require('assert') 4 | const fs = require('fs') 5 | const is = require('is-type-of') 6 | const co = require('co') 7 | const BaseContextClass = require('./utils/base_context_class') 8 | const utils = require('./utils') 9 | const Router = require('@eggjs/router').EggRouter 10 | const Timing = require('./utils/timing') 11 | const Lifecycle = require('./lifecycle') 12 | 13 | const DEPRECATE = Symbol('EggCore#deprecate') 14 | const ROUTER = Symbol('EggCore#router') 15 | const EGG_LOADER = Symbol.for('egg#loader') 16 | const CLOSE_PROMISE = Symbol('EggCore#closePromise') 17 | 18 | export default class Core extends KoaApplication { 19 | timing: any 20 | _options: any 21 | options: any 22 | BaseContextClass: any 23 | Controller: any 24 | Service: any 25 | lifecycle: any 26 | loader: any 27 | console: any 28 | constructor (options: any = {}) { 29 | options.baseDir = options.baseDir || process.cwd() 30 | options.type = options.type || 'application' 31 | 32 | assert(typeof options.baseDir === 'string', 'options.baseDir required, and must be a string') 33 | assert(fs.existsSync(options.baseDir), `Directory ${options.baseDir} not exists`) 34 | assert(fs.statSync(options.baseDir).isDirectory(), `Directory ${options.baseDir} is not a directory`) 35 | assert(options.type === 'application' || options.type === 'agent', 'options.type should be application or agent') 36 | 37 | super() 38 | 39 | this.timing = new Timing() 40 | 41 | // cache deprecate object by file 42 | this[DEPRECATE] = new Map() 43 | 44 | this._options = this.options = options 45 | this.deprecate.property(this, '_options', 'app._options is deprecated, use app.options instead') 46 | 47 | this.BaseContextClass = BaseContextClass 48 | 49 | const Controller = this.BaseContextClass 50 | 51 | this.Controller = Controller 52 | 53 | const Service = this.BaseContextClass 54 | 55 | this.Service = Service 56 | 57 | this.lifecycle = new Lifecycle({ 58 | baseDir: options.baseDir, 59 | app: this 60 | }) 61 | this.lifecycle.on('error', err => this.emit('error', err)) 62 | this.lifecycle.on('ready_timeout', id => this.emit('ready_timeout', id)) 63 | this.lifecycle.on('ready_stat', data => this.emit('ready_stat', data)) 64 | 65 | const Loader = this[EGG_LOADER] 66 | assert(Loader, 'Symbol.for(\'egg#loader\') is required') 67 | this.loader = new Loader({ 68 | baseDir: options.baseDir, 69 | app: this, 70 | plugins: options.plugins, 71 | logger: this.console, 72 | serverScope: options.serverScope, 73 | env: options.env 74 | }) 75 | } 76 | 77 | use (fn: any): any { 78 | assert(is.function(fn), 'app.use() requires a function') 79 | this.middleware.push(utils.middleware(fn)) 80 | return this 81 | } 82 | 83 | get type () { 84 | return this.options.type 85 | } 86 | 87 | get baseDir () { 88 | return this.options.baseDir 89 | } 90 | 91 | get deprecate () { 92 | const caller = utils.getCalleeFromStack() 93 | if (!this[DEPRECATE].has(caller)) { 94 | const deprecate = require('depd')('egg') 95 | deprecate._file = caller 96 | this[DEPRECATE].set(caller, deprecate) 97 | } 98 | return this[DEPRECATE].get(caller) 99 | } 100 | 101 | get name () { 102 | return this.loader ? this.loader.pkg.name : '' 103 | } 104 | 105 | get plugins () { 106 | return this.loader ? this.loader.plugins : {} 107 | } 108 | 109 | get config () { 110 | return this.loader ? this.loader.config : {} 111 | } 112 | 113 | beforeStart (scope) { 114 | this.lifecycle.registerBeforeStart(scope) 115 | } 116 | 117 | ready (flagOrFunction) { 118 | return this.lifecycle.ready(flagOrFunction) 119 | } 120 | 121 | readyCallback (name, opts) { 122 | return this.lifecycle.legacyReadyCallback(name, opts) 123 | } 124 | 125 | beforeClose (fn) { 126 | this.lifecycle.registerBeforeClose(fn) 127 | } 128 | 129 | async close () { 130 | if (this[CLOSE_PROMISE]) return this[CLOSE_PROMISE] 131 | this[CLOSE_PROMISE] = this.lifecycle.close() 132 | return this[CLOSE_PROMISE] 133 | } 134 | 135 | get router () { 136 | if (this[ROUTER]) { 137 | return this[ROUTER] 138 | } 139 | const router = this[ROUTER] = new Router({ sensitive: true }, this) 140 | // register router middleware 141 | this.beforeStart(() => { 142 | this.use(router.middleware()) 143 | }) 144 | return router 145 | } 146 | 147 | url (name, params) { 148 | return this.router.url(name, params) 149 | } 150 | 151 | del (...args) { 152 | this.router.delete(...args) 153 | return this 154 | } 155 | 156 | get [EGG_LOADER] () { 157 | return require('./loader/egg_loader') 158 | } 159 | 160 | // toAsyncFunction (fn) { 161 | // if (!is.generatorFunction(fn)) return fn 162 | // fn = co.wrap(fn) 163 | // return async function (...args) { 164 | // return fn.apply(this, args) 165 | // } 166 | // } 167 | 168 | toPromise (obj) { 169 | return co(function * () { 170 | return yield obj 171 | }) 172 | } 173 | } 174 | 175 | // delegate all router method to application 176 | utils.methods.concat(['all', 'resources', 'register', 'redirect']).forEach(method => { 177 | Core.prototype[method] = function (...args) { 178 | this.router[method](...args) 179 | return this 180 | } 181 | }) 182 | -------------------------------------------------------------------------------- /demo/koa/extend/context.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // eslint-disable-next-line no-unused-vars 3 | import KoaApplication = require('koa'); 4 | 5 | const util = require('util') 6 | const createError = require('http-errors') 7 | const httpAssert = require('http-assert') 8 | const delegate = require('delegates') 9 | const statuses = require('statuses') 10 | const Cookies = require('cookies') 11 | 12 | const COOKIES = Symbol('context#cookies') 13 | 14 | const proto: KoaApplication.Context | any = { 15 | inspect () { 16 | // eslint-disable-next-line no-undef 17 | if (this === proto) return this 18 | return this.toJSON() 19 | }, 20 | 21 | toJSON (): object { 22 | return { 23 | request: this.request.toJSON(), 24 | response: this.response.toJSON(), 25 | app: this.app.toJSON(), 26 | originalUrl: this.originalUrl, 27 | req: '', 28 | res: '', 29 | socket: '' 30 | } 31 | }, 32 | assert: httpAssert, 33 | 34 | /** 35 | * Throw an error with `status` (default 500) and 36 | * `msg`. Note that these are user-level 37 | * errors, and the message may be exposed to the client. 38 | * 39 | * this.throw(403) 40 | * this.throw(400, 'name required') 41 | * this.throw('something exploded') 42 | * this.throw(new Error('invalid')) 43 | * this.throw(400, new Error('invalid')) 44 | * 45 | * See: https://github.com/jshttp/http-errors 46 | * 47 | * Note: `status` should only be passed as the first parameter. 48 | * 49 | * @param {String|Number|Error} err, msg or status 50 | * @param {String|Number|Error} [err, msg or status] 51 | * @param {Object} [props] 52 | * @api public 53 | */ 54 | 55 | throw (...args) { 56 | throw createError(...args) 57 | }, 58 | 59 | /** 60 | * Default error handling. 61 | * 62 | * @param {Error} err 63 | * @api private 64 | */ 65 | 66 | onerror (err) { 67 | // don't do anything if there is no error. 68 | // this allows you to pass `this.onerror` 69 | // to node-style callbacks. 70 | if (err == null) return 71 | 72 | if (!(err instanceof Error)) err = new Error(util.format('non-error thrown: %j', err)) 73 | 74 | let headerSent = false 75 | if (this.headerSent || !this.writable) { 76 | headerSent = err.headerSent = true 77 | } 78 | 79 | // delegate 80 | this.app.emit('error', err, this) 81 | 82 | // nothing we can do here other 83 | // than delegate to the app-level 84 | // handler and log. 85 | if (headerSent) { 86 | return 87 | } 88 | 89 | const { res } = this 90 | 91 | // first unset all headers 92 | /* istanbul ignore else */ 93 | if (typeof res.getHeaderNames === 'function') { 94 | res.getHeaderNames().forEach(name => res.removeHeader(name)) 95 | } else { 96 | res._headers = {} // Node < 7.7 97 | } 98 | 99 | // then set those specified 100 | this.set(err.headers) 101 | 102 | // force text/plain 103 | this.type = 'text' 104 | 105 | // ENOENT support 106 | if (err.code === 'ENOENT') err.status = 404 107 | 108 | // default to 500 109 | if (typeof err.status !== 'number' || !statuses[err.status]) err.status = 500 110 | 111 | // respond 112 | const code = statuses[err.status] 113 | const msg = err.expose ? err.message : code 114 | this.status = err.status 115 | this.length = Buffer.byteLength(msg) 116 | res.end(msg) 117 | }, 118 | 119 | get cookies () { 120 | if (!this[COOKIES]) { 121 | this[COOKIES] = new Cookies(this.req, this.res, { 122 | keys: this.app.keys, 123 | secure: this.request.secure 124 | }) 125 | } 126 | return this[COOKIES] 127 | }, 128 | 129 | set cookies (_cookies) { 130 | this[COOKIES] = _cookies 131 | } 132 | } 133 | 134 | /** 135 | * Custom inspection implementation for newer Node.js versions. 136 | * 137 | * @return {Object} 138 | * @api public 139 | */ 140 | 141 | /* istanbul ignore else */ 142 | if (util.inspect.custom) { 143 | module.exports[util.inspect.custom] = module.exports.inspect 144 | } 145 | 146 | /** 147 | * Response delegation. 148 | */ 149 | 150 | delegate(proto, 'response') 151 | .method('attachment') 152 | .method('redirect') 153 | .method('remove') 154 | .method('vary') 155 | .method('has') 156 | .method('set') 157 | .method('append') 158 | .method('flushHeaders') 159 | .access('status') 160 | .access('message') 161 | .access('body') 162 | .access('length') 163 | .access('type') 164 | .access('lastModified') 165 | .access('etag') 166 | .getter('headerSent') 167 | .getter('writable') 168 | 169 | /** 170 | * Request delegation. 171 | */ 172 | 173 | delegate(proto, 'request') 174 | .method('acceptsLanguages') 175 | .method('acceptsEncodings') 176 | .method('acceptsCharsets') 177 | .method('accepts') 178 | .method('get') 179 | .method('is') 180 | .access('querystring') 181 | .access('idempotent') 182 | .access('socket') 183 | .access('search') 184 | .access('method') 185 | .access('query') 186 | .access('path') 187 | .access('url') 188 | .access('accept') 189 | .getter('origin') 190 | .getter('href') 191 | .getter('subdomains') 192 | .getter('protocol') 193 | .getter('host') 194 | .getter('hostname') 195 | .getter('URL') 196 | .getter('header') 197 | .getter('headers') 198 | .getter('secure') 199 | .getter('stale') 200 | .getter('fresh') 201 | .getter('ips') 202 | .getter('ip') 203 | 204 | export default proto 205 | -------------------------------------------------------------------------------- /demo/koa/extend/application.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import context from './context' 3 | import request from './request' 4 | import response from './response' 5 | import http from 'http' 6 | 7 | const isGeneratorFunction = require('is-generator-function') 8 | const debug = require('debug')('koa:application') 9 | const onFinished = require('on-finished') 10 | const compose = require('koa-compose') 11 | const statuses = require('statuses') 12 | const Emitter = require('events') 13 | const util = require('util') 14 | const Stream = require('stream') 15 | const only = require('only') 16 | const convert = require('koa-convert') 17 | const deprecate = require('depd')('koa') 18 | // const { HttpError } = require('http-errors') 19 | 20 | export default class Application extends Emitter { 21 | constructor (options?) { 22 | super() 23 | options = options || {} 24 | this.proxy = options.proxy || false 25 | this.subdomainOffset = options.subdomainOffset || 2 26 | this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For' 27 | this.maxIpsCount = options.maxIpsCount || 0 28 | this.env = options.env || process.env.NODE_ENV || 'development' 29 | if (options.keys) this.keys = options.keys 30 | this.middleware = [] 31 | this.context = Object.create(context) 32 | this.request = Object.create(request) 33 | this.response = Object.create(response) 34 | if (util.inspect.custom) { 35 | this[util.inspect.custom] = this.inspect 36 | } 37 | } 38 | 39 | listen (...args) { 40 | debug('listen') 41 | const server = http.createServer(this.callback()) 42 | return server.listen(...args) 43 | } 44 | 45 | /** 46 | * Return JSON representation. 47 | * We only bother showing settings. 48 | * 49 | * @return {Object} 50 | * @api public 51 | */ 52 | 53 | toJSON () { 54 | return only(this, [ 55 | 'subdomainOffset', 56 | 'proxy', 57 | 'env' 58 | ]) 59 | } 60 | 61 | /** 62 | * Inspect implementation. 63 | * 64 | * @return {Object} 65 | * @api public 66 | */ 67 | 68 | inspect () { 69 | return this.toJSON() 70 | } 71 | 72 | /** 73 | * Use the given middleware `fn`. 74 | * 75 | * Old-style middleware will be converted. 76 | * 77 | * @param {Function} fn 78 | * @return {Application} self 79 | * @api public 80 | */ 81 | 82 | use (fn) { 83 | if (typeof fn !== 'function') throw new TypeError('middleware must be a function!') 84 | if (isGeneratorFunction(fn)) { 85 | deprecate('Support for generators will be removed in v3. ' + 86 | 'See the documentation for examples of how to convert old middleware ' + 87 | 'https://github.com/koajs/koa/blob/master/docs/migration.md') 88 | fn = convert(fn) 89 | } 90 | debug('use %s', fn._name || fn.name || '-') 91 | this.middleware.push(fn) 92 | return this 93 | } 94 | 95 | /** 96 | * Return a request handler callback 97 | * for node's native http server. 98 | * 99 | * @return {Function} 100 | * @api public 101 | */ 102 | 103 | callback () { 104 | const fn = compose(this.middleware) 105 | 106 | if (!this.listenerCount('error')) this.on('error', this.onerror) 107 | 108 | const handleRequest = (req, res) => { 109 | const ctx = this.createContext(req, res) 110 | return this.handleRequest(ctx, fn) 111 | } 112 | 113 | return handleRequest 114 | } 115 | 116 | /** 117 | * Handle request in callback. 118 | * 119 | * @api private 120 | */ 121 | 122 | handleRequest (ctx, fnMiddleware) { 123 | const res = ctx.res 124 | res.statusCode = 404 125 | const onerror = err => ctx.onerror(err) 126 | const handleResponse = () => respond(ctx) 127 | onFinished(res, onerror) 128 | return fnMiddleware(ctx).then(handleResponse).catch(onerror) 129 | } 130 | 131 | /** 132 | * Initialize a new context. 133 | * 134 | * @api private 135 | */ 136 | 137 | createContext (req, res) { 138 | const context = Object.create(this.context) 139 | const request = context.request = Object.create(this.request) 140 | const response = context.response = Object.create(this.response) 141 | context.app = request.app = response.app = this 142 | context.req = request.req = response.req = req 143 | context.res = request.res = response.res = res 144 | request.ctx = response.ctx = context 145 | request.response = response 146 | response.request = request 147 | context.originalUrl = request.originalUrl = req.url 148 | context.state = {} 149 | return context 150 | } 151 | 152 | /** 153 | * Default error handler. 154 | * 155 | * @param {Error} err 156 | * @api private 157 | */ 158 | 159 | onerror (err: any) { 160 | if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err)) 161 | 162 | if ((err as any).status === 404 || (err as any).expose) return 163 | if (this.silent) return 164 | 165 | const msg = err.stack || err.toString() 166 | console.error() 167 | console.error(msg.replace(/^/gm, ' ')) 168 | console.error() 169 | } 170 | } 171 | 172 | /** 173 | * Response helper. 174 | */ 175 | 176 | function respond (ctx) { 177 | // allow bypassing koa 178 | if (ctx.respond === false) return 179 | 180 | if (!ctx.writable) return 181 | 182 | const res = ctx.res 183 | let body = ctx.body 184 | const code = ctx.status 185 | 186 | // ignore body 187 | if (statuses.empty[code]) { 188 | // strip headers 189 | ctx.body = null 190 | return res.end() 191 | } 192 | 193 | if (ctx.method === 'HEAD') { 194 | if (!res.headersSent && !ctx.response.has('Content-Length')) { 195 | const { length } = ctx.response 196 | if (Number.isInteger(length)) ctx.length = length 197 | } 198 | return res.end() 199 | } 200 | 201 | // status body 202 | if (body == null) { 203 | if (ctx.response._explicitNullBody) { 204 | ctx.response.remove('Content-Type') 205 | ctx.response.remove('Transfer-Encoding') 206 | return res.end() 207 | } 208 | if (ctx.req.httpVersionMajor >= 2) { 209 | body = String(code) 210 | } else { 211 | body = ctx.message || String(code) 212 | } 213 | if (!res.headersSent) { 214 | ctx.type = 'text' 215 | ctx.length = Buffer.byteLength(body) 216 | } 217 | return res.end(body) 218 | } 219 | 220 | // responses 221 | if (Buffer.isBuffer(body)) return res.end(body) 222 | if (typeof body === 'string') return res.end(body) 223 | if (body instanceof Stream) return body.pipe(res) 224 | 225 | // body: json 226 | body = JSON.stringify(body) 227 | if (!res.headersSent) { 228 | ctx.length = Buffer.byteLength(body) 229 | } 230 | res.end(body) 231 | } 232 | -------------------------------------------------------------------------------- /packages/core/lib/lifecycle.ts: -------------------------------------------------------------------------------- 1 | import utils from './utils' 2 | import { EventEmitter } from 'events' 3 | import is from 'is-type-of' 4 | import assert from 'assert' 5 | 6 | const getReady = require('get-ready') 7 | const { Ready } = require('ready-callback') 8 | const debug = require('debug')('egg-core:lifecycle') 9 | 10 | const INIT = Symbol('Lifycycle#init') 11 | const INIT_READY = Symbol('Lifecycle#initReady') 12 | const DELEGATE_READY_EVENT = Symbol('Lifecycle#delegateReadyEvent') 13 | const REGISTER_READY_CALLBACK = Symbol('Lifecycle#registerReadyCallback') 14 | const CLOSE_SET = Symbol('Lifecycle#closeSet') 15 | const IS_CLOSED = Symbol('Lifecycle#isClosed') 16 | const BOOT_HOOKS = Symbol('Lifecycle#bootHooks') 17 | const BOOTS = Symbol('Lifecycle#boots') 18 | 19 | export default class Lifecycle extends EventEmitter { 20 | options: any 21 | ready (arg0: (err: any) => void) { 22 | throw new Error('Method not implemented.') 23 | } 24 | 25 | readyTimeout: number 26 | loadReady: any 27 | bootReady: any 28 | /** 29 | * @param {object} options - options 30 | * @param {String} options.baseDir - the directory of application 31 | * @param {EggCore} options.app - Application instance 32 | * @param {Logger} options.logger - logger 33 | */ 34 | constructor (options) { 35 | super() 36 | this.options = options 37 | this[BOOT_HOOKS] = [] 38 | this[BOOTS] = [] 39 | this[CLOSE_SET] = new Set() 40 | this[IS_CLOSED] = false 41 | this[INIT] = false 42 | getReady.mixin(this) 43 | 44 | this.timing.start('Application Start') 45 | 46 | this[INIT_READY]() 47 | this 48 | .on('ready_stat', data => { 49 | this.logger.info('[egg:core:ready_stat] end ready task %s, remain %j', data.id, data.remain) 50 | }) 51 | .on('ready_timeout', id => { 52 | this.logger.warn('[egg:core:ready_timeout] %s seconds later %s was still unable to finish.', this.readyTimeout / 1000, id) 53 | }) 54 | 55 | this.ready(err => { 56 | this.triggerDidReady(err) 57 | this.timing.end('Application Start') 58 | }) 59 | } 60 | 61 | get app () { 62 | return this.options.app 63 | } 64 | 65 | get logger () { 66 | return this.options.logger 67 | } 68 | 69 | get timing () { 70 | return this.app.timing 71 | } 72 | 73 | legacyReadyCallback (name, opt) { 74 | return this.loadReady.readyCallback(name, opt) 75 | } 76 | 77 | addBootHook (hook) { 78 | assert(this[INIT] === false, 'do not add hook when lifecycle has been initialized') 79 | this[BOOT_HOOKS].push(hook) 80 | } 81 | 82 | addFunctionAsBootHook (hook) { 83 | assert(this[INIT] === false, 'do not add hook when lifecycle has been initialized') 84 | // app.js is exported as a function 85 | // call this function in configDidLoad 86 | this[BOOT_HOOKS].push(class Hook { 87 | app: any 88 | constructor (app) { 89 | this.app = app 90 | } 91 | 92 | configDidLoad () { 93 | hook(this.app) 94 | } 95 | }) 96 | } 97 | 98 | /** 99 | * init boots and trigger config did config 100 | */ 101 | init () { 102 | assert(this[INIT] === false, 'lifecycle have been init') 103 | this[INIT] = true 104 | this[BOOTS] = this[BOOT_HOOKS].map(t => new t(this.app)) 105 | } 106 | 107 | registerBeforeStart (scope) { 108 | this[REGISTER_READY_CALLBACK]({ 109 | scope, 110 | ready: this.loadReady, 111 | timingKeyPrefix: 'Before Start' 112 | } as any) 113 | } 114 | 115 | registerBeforeClose (fn) { 116 | assert(is.function(fn), 'argument should be function') 117 | assert(this[IS_CLOSED] === false, 'app has been closed') 118 | this[CLOSE_SET].add(fn) 119 | } 120 | 121 | async close () { 122 | // close in reverse order: first created, last closed 123 | const closeFns = Array.from(this[CLOSE_SET]) 124 | for (const fn of closeFns.reverse()) { 125 | await utils.callFn(fn) 126 | this[CLOSE_SET].delete(fn) 127 | } 128 | // Be called after other close callbacks 129 | this.app.emit('close') 130 | this.removeAllListeners() 131 | this.app.removeAllListeners() 132 | this[IS_CLOSED] = true 133 | } 134 | 135 | triggerConfigWillLoad () { 136 | for (const boot of this[BOOTS]) { 137 | if (boot.configWillLoad) { 138 | boot.configWillLoad() 139 | } 140 | } 141 | this.triggerConfigDidLoad() 142 | } 143 | 144 | triggerConfigDidLoad () { 145 | for (const boot of this[BOOTS]) { 146 | if (boot.configDidLoad) { 147 | boot.configDidLoad() 148 | } 149 | // function boot hook register after configDidLoad trigger 150 | const beforeClose = boot.beforeClose && boot.beforeClose.bind(boot) 151 | if (beforeClose) { 152 | this.registerBeforeClose(beforeClose) 153 | } 154 | } 155 | this.triggerDidLoad() 156 | } 157 | 158 | triggerDidLoad () { 159 | debug('register didLoad') 160 | for (const boot of this[BOOTS]) { 161 | const didLoad = boot.didLoad && boot.didLoad.bind(boot) 162 | if (didLoad) { 163 | this[REGISTER_READY_CALLBACK]({ 164 | scope: didLoad, 165 | ready: this.loadReady, 166 | timingKeyPrefix: 'Did Load', 167 | scopeFullName: boot.fullPath + ':didLoad' 168 | }) 169 | } 170 | } 171 | } 172 | 173 | triggerWillReady () { 174 | debug('register willReady') 175 | this.bootReady.start() 176 | for (const boot of this[BOOTS]) { 177 | const willReady = boot.willReady && boot.willReady.bind(boot) 178 | if (willReady) { 179 | this[REGISTER_READY_CALLBACK]({ 180 | scope: willReady, 181 | ready: this.bootReady, 182 | timingKeyPrefix: 'Will Ready', 183 | scopeFullName: boot.fullPath + ':willReady' 184 | }) 185 | } 186 | } 187 | } 188 | 189 | triggerDidReady (err) { 190 | debug('trigger didReady'); 191 | (async () => { 192 | for (const boot of this[BOOTS]) { 193 | if (boot.didReady) { 194 | try { 195 | await boot.didReady(err) 196 | } catch (e) { 197 | this.emit('error', e) 198 | } 199 | } 200 | } 201 | debug('trigger didReady done') 202 | })() 203 | } 204 | 205 | triggerServerDidReady () { 206 | (async () => { 207 | for (const boot of this[BOOTS]) { 208 | try { 209 | await utils.callFn(boot.serverDidReady, null, boot) 210 | } catch (e) { 211 | this.emit('error', e) 212 | } 213 | } 214 | })() 215 | } 216 | 217 | [INIT_READY] () { 218 | this.loadReady = new Ready({ timeout: this.readyTimeout }) 219 | this[DELEGATE_READY_EVENT](this.loadReady) 220 | this.loadReady.ready(err => { 221 | debug('didLoad done') 222 | if (err) { 223 | this.ready(err) 224 | } else { 225 | this.triggerWillReady() 226 | } 227 | }) 228 | 229 | this.bootReady = new Ready({ timeout: this.readyTimeout, lazyStart: true }) 230 | this[DELEGATE_READY_EVENT](this.bootReady) 231 | this.bootReady.ready(err => { 232 | this.ready(err || true) 233 | }) 234 | } 235 | 236 | [DELEGATE_READY_EVENT] (ready) { 237 | ready.once('error', err => ready.ready(err)) 238 | ready.on('ready_timeout', id => this.emit('ready_timeout', id)) 239 | ready.on('ready_stat', data => this.emit('ready_stat', data)) 240 | ready.on('error', err => this.emit('error', err)) 241 | } 242 | 243 | [REGISTER_READY_CALLBACK] ({ scope, ready, timingKeyPrefix, scopeFullName }) { 244 | if (!is.function(scope)) { 245 | throw new Error('boot only support function') 246 | } 247 | 248 | const name = scopeFullName || utils.getCalleeFromStack(true, 4) 249 | const timingkey = `${timingKeyPrefix} in ` + utils.getResolvedFilename(name, this.app.baseDir) 250 | 251 | this.timing.start(timingkey) 252 | 253 | const done = ready.readyCallback(name) 254 | 255 | process.nextTick(() => { 256 | utils.callFn(scope).then(() => { 257 | done() 258 | this.timing.end(timingkey) 259 | }, err => { 260 | done(err) 261 | this.timing.end(timingkey) 262 | }) 263 | }) 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /packages/core/lib/loader/file_loader.ts: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const fs = require('fs') 3 | const debug = require('debug')('egg-core:loader') 4 | const path = require('path') 5 | const globby = require('globby') 6 | const is = require('is-type-of') 7 | const utils = require('../utils') 8 | const FULLPATH = Symbol('EGG_LOADER_ITEM_FULLPATH') 9 | const EXPORTS = Symbol('EGG_LOADER_ITEM_EXPORTS') 10 | 11 | const defaults = { 12 | directory: null, 13 | target: null, 14 | match: undefined, 15 | ignore: undefined, 16 | lowercaseFirst: false, 17 | caseStyle: 'camel', 18 | initializer: null, 19 | call: true, 20 | override: false, 21 | inject: undefined, 22 | filter: null 23 | } 24 | 25 | type FileLoaderOptions = { 26 | directory: any; 27 | target: any; 28 | match?: any; 29 | ignore?: any; 30 | lowercaseFirst?: boolean; 31 | caseStyle?: any; 32 | initializer?: any; 33 | call?: boolean; 34 | override?: boolean; 35 | inject?: any; 36 | filter?: any 37 | } 38 | 39 | export default class FileLoader { 40 | options: { 41 | directory: any; 42 | target: any; 43 | match?: any; 44 | ignore?: any; 45 | lowercaseFirst?: boolean; 46 | caseStyle: any; 47 | initializer: any; 48 | call: boolean; 49 | override?: boolean; 50 | inject: any; 51 | filter?: any 52 | } & FileLoaderOptions 53 | 54 | /** 55 | * @class 56 | * @param {Object} options - options 57 | * @param {String|Array} options.directory - directories to be loaded 58 | * @param {Object} options.target - attach the target object from loaded files 59 | * @param {String} options.match - match the files when load, support glob, default to all js files 60 | * @param {String} options.ignore - ignore the files when load, support glob 61 | * @param {Function} options.initializer - custom file exports, receive two parameters, first is the inject object(if not js file, will be content buffer), second is an `options` object that contain `path` 62 | * @param {Boolean} options.call - determine whether invoke when exports is function 63 | * @param {Boolean} options.override - determine whether override the property when get the same name 64 | * @param {Object} options.inject - an object that be the argument when invoke the function 65 | * @param {Function} options.filter - a function that filter the exports which can be loaded 66 | * @param {String|Function} options.caseStyle - set property's case when converting a filepath to property list. 67 | */ 68 | constructor (options: FileLoaderOptions) { 69 | this.options = Object.assign({}, defaults, options) 70 | } 71 | 72 | load () { 73 | const items = this.parse() 74 | const target = this.options.target 75 | for (const item of items) { 76 | debug('loading item %j', item) 77 | // item { properties: [ 'a', 'b', 'c'], exports } 78 | // => target.a.b.c = exports 79 | item.properties.reduce((target, property, index) => { 80 | let obj 81 | const properties = item.properties.slice(0, index + 1).join('.') 82 | if (index === item.properties.length - 1) { 83 | if (property in target) { 84 | if (!this.options.override) throw new Error(`can't overwrite property '${properties}' from ${target[property][FULLPATH]} by ${item.fullpath}`) 85 | } 86 | obj = item.exports 87 | if (obj && !is.primitive(obj)) { 88 | obj[FULLPATH] = item.fullpath 89 | obj[EXPORTS] = true 90 | } 91 | } else { 92 | obj = target[property] || {} 93 | } 94 | target[property] = obj 95 | debug('loaded %s', properties) 96 | return obj 97 | }, target) 98 | } 99 | 100 | return target 101 | } 102 | 103 | parse () { 104 | let files = this.options.match 105 | if (!files) { 106 | files = ['**/*.(js|ts)', '!**/*.d.ts'] 107 | } else { 108 | files = Array.isArray(files) ? files : [files] 109 | } 110 | 111 | let ignore = this.options.ignore 112 | if (ignore) { 113 | ignore = Array.isArray(ignore) ? ignore : [ignore] 114 | ignore = ignore.filter(f => !!f).map(f => '!' + f) 115 | files = files.concat(ignore) 116 | } 117 | 118 | let directories = this.options.directory 119 | if (!Array.isArray(directories)) { 120 | directories = [directories] 121 | } 122 | 123 | const filter = is.function(this.options.filter) ? this.options.filter : null 124 | const items = [] 125 | debug('parsing %j', directories) 126 | for (const directory of directories) { 127 | const filepaths = globby.sync(files, { cwd: directory }) 128 | for (const filepath of filepaths) { 129 | const fullpath = path.join(directory, filepath) 130 | if (!fs.statSync(fullpath).isFile()) continue 131 | // get properties 132 | // app/service/foo/bar.js => [ 'foo', 'bar' ] 133 | const properties = getProperties(filepath, this.options) 134 | // app/service/foo/bar.js => service.foo.bar 135 | const pathName = directory.split(/[/\\]/).slice(-1) + '.' + properties.join('.') 136 | // get exports from the file 137 | const exports = getExports(fullpath, this.options, pathName) 138 | 139 | // ignore exports when it's null or false returned by filter function 140 | if (exports == null || (filter && filter(exports) === false)) continue 141 | 142 | // set properties of class 143 | if (is.class(exports)) { 144 | exports.prototype.pathName = pathName 145 | exports.prototype.fullPath = fullpath 146 | } 147 | 148 | items.push({ fullpath, properties, exports }) 149 | debug('parse %s, properties %j, export %j', fullpath, properties, exports) 150 | } 151 | } 152 | 153 | return items 154 | } 155 | } 156 | 157 | module.exports = FileLoader 158 | module.exports.EXPORTS = EXPORTS 159 | module.exports.FULLPATH = FULLPATH 160 | 161 | // convert file path to an array of properties 162 | // a/b/c.js => ['a', 'b', 'c'] 163 | function getProperties (filepath, { caseStyle }) { 164 | // if caseStyle is function, return the result of function 165 | if (is.function(caseStyle)) { 166 | const result = caseStyle(filepath) 167 | assert(is.array(result), `caseStyle expect an array, but got ${result}`) 168 | return result 169 | } 170 | // use default camelize 171 | return defaultCamelize(filepath, caseStyle) 172 | } 173 | 174 | // Get exports from filepath 175 | // If exports is null/undefined, it will be ignored 176 | function getExports (fullpath, { initializer, call, inject }, pathName) { 177 | let exports = utils.loadFile(fullpath) 178 | // process exports as you like 179 | if (initializer) { 180 | exports = initializer(exports, { path: fullpath, pathName }) 181 | } 182 | 183 | if (is.class(exports) || is.generatorFunction(exports) || is.asyncFunction(exports)) { 184 | return exports 185 | } 186 | 187 | if (call && is.function(exports)) { 188 | exports = exports(inject) 189 | if (exports != null) { 190 | return exports 191 | } 192 | } 193 | 194 | return exports 195 | } 196 | 197 | function defaultCamelize (filepath, caseStyle) { 198 | const properties = filepath.substring(0, filepath.lastIndexOf('.')).split('/') 199 | return properties.map(property => { 200 | if (!/^[a-z][a-z0-9_-]*$/i.test(property)) { 201 | throw new Error(`${property} is not match 'a-z0-9_-' in ${filepath}`) 202 | } 203 | 204 | // foo_bar.js > FooBar 205 | // fooBar.js > FooBar 206 | // FooBar.js > FooBar 207 | // FooBar.js > FooBar 208 | // FooBar.js > fooBar (if lowercaseFirst is true) 209 | property = property.replace(/[_-][a-z]/ig, s => s.substring(1).toUpperCase()) 210 | let first = property[0] 211 | switch (caseStyle) { 212 | case 'lower': 213 | first = first.toLowerCase() 214 | break 215 | case 'upper': 216 | first = first.toUpperCase() 217 | break 218 | case 'camel': 219 | default: 220 | } 221 | return first + property.substring(1) 222 | }) 223 | } 224 | 225 | const target = {} 226 | 227 | new FileLoader({ 228 | directory: '.', 229 | target 230 | }) 231 | 232 | console.log('target', target) 233 | -------------------------------------------------------------------------------- /packages/core/lib/loader/egg_loader.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const assert = require('assert') 6 | const is = require('is-type-of') 7 | const debug = require('debug')('egg-core') 8 | const homedir = require('node-homedir') 9 | const FileLoader = require('./file_loader') 10 | const ContextLoader = require('./context_loader') 11 | const utility = require('utility') 12 | const utils = require('../utils') 13 | const Timing = require('../utils/timing') 14 | 15 | const REQUIRE_COUNT = Symbol('EggLoader#requireCount') 16 | 17 | export default class EggLoader { 18 | options: any 19 | app: any 20 | lifecycle: any 21 | timing: any 22 | pkg: any 23 | eggPaths: any[] 24 | serverEnv: any 25 | appInfo: { 26 | name: any 27 | baseDir: any 28 | env: any 29 | scope: any 30 | HOME: any 31 | pkg: any 32 | root: any 33 | } 34 | 35 | serverScope: any 36 | dirs: any 37 | orderPlugins: any 38 | /** 39 | * @class 40 | * @param {Object} options - options 41 | * @param {String} options.baseDir - the directory of application 42 | * @param {EggCore} options.app - Application instance 43 | * @param {Logger} options.logger - logger 44 | * @param {Object} [options.plugins] - custom plugins 45 | * @since 1.0.0 46 | */ 47 | constructor (options) { 48 | this.options = options 49 | assert(fs.existsSync(this.options.baseDir), `${this.options.baseDir} not exists`) 50 | assert(this.options.app, 'options.app is required') 51 | assert(this.options.logger, 'options.logger is required') 52 | 53 | this.app = this.options.app 54 | this.lifecycle = this.app.lifecycle 55 | this.timing = this.app.timing || new Timing() 56 | this[REQUIRE_COUNT] = 0 57 | 58 | /** 59 | * @member {Object} EggLoader#pkg 60 | * @see {@link AppInfo#pkg} 61 | * @since 1.0.0 62 | */ 63 | this.pkg = utility.readJSONSync(path.join(this.options.baseDir, 'package.json')) 64 | 65 | this.eggPaths = this.getEggPaths() 66 | debug('Loaded eggPaths %j', this.eggPaths) 67 | 68 | this.serverEnv = this.getServerEnv() 69 | debug('Loaded serverEnv %j', this.serverEnv) 70 | 71 | this.appInfo = this.getAppInfo() 72 | 73 | this.serverScope = options.serverScope !== undefined 74 | ? options.serverScope 75 | : this.getServerScope() 76 | } 77 | 78 | getServerEnv () { 79 | let serverEnv = this.options.env 80 | 81 | const envPath = path.join(this.options.baseDir, 'config/env') 82 | if (!serverEnv && fs.existsSync(envPath)) { 83 | serverEnv = fs.readFileSync(envPath, 'utf8').trim() 84 | } 85 | 86 | if (!serverEnv) { 87 | serverEnv = process.env.EGG_SERVER_ENV 88 | } 89 | 90 | if (!serverEnv) { 91 | if (process.env.NODE_ENV === 'test') { 92 | serverEnv = 'unittest' 93 | } else if (process.env.NODE_ENV === 'production') { 94 | serverEnv = 'prod' 95 | } else { 96 | serverEnv = 'local' 97 | } 98 | } else { 99 | serverEnv = serverEnv.trim() 100 | } 101 | 102 | return serverEnv 103 | } 104 | 105 | getServerScope () { 106 | return process.env.EGG_SERVER_SCOPE || '' 107 | } 108 | 109 | getAppname () { 110 | if (this.pkg.name) { 111 | debug('Loaded appname(%s) from package.json', this.pkg.name) 112 | return this.pkg.name 113 | } 114 | const pkg = path.join(this.options.baseDir, 'package.json') 115 | throw new Error(`name is required from ${pkg}`) 116 | } 117 | 118 | getHomedir () { 119 | return process.env.EGG_HOME || homedir() || '/home/admin' 120 | } 121 | 122 | getAppInfo () { 123 | const env = this.serverEnv 124 | const scope = this.serverScope 125 | const home = this.getHomedir() 126 | const baseDir = this.options.baseDir 127 | 128 | return { 129 | name: this.getAppname(), 130 | baseDir, 131 | env, 132 | scope, 133 | HOME: home, 134 | pkg: this.pkg, 135 | root: env === 'local' || env === 'unittest' ? baseDir : home 136 | } 137 | } 138 | 139 | getEggPaths () { 140 | // avoid require recursively 141 | const EggCore = require('../egg') 142 | const eggPaths = [] 143 | 144 | let proto = this.app 145 | 146 | // Loop for the prototype chain 147 | while (proto) { 148 | proto = Object.getPrototypeOf(proto) 149 | // stop the loop if 150 | // - object extends Object 151 | // - object extends EggCore 152 | if (proto === Object.prototype || proto === EggCore.prototype) { 153 | break 154 | } 155 | 156 | const eggPath = proto[Symbol.for('egg#eggPath')] 157 | assert(eggPath && typeof eggPath === 'string', 'Symbol.for(\'egg#eggPath\') should be string') 158 | assert(fs.existsSync(eggPath), `${eggPath} not exists`) 159 | const realpath = fs.realpathSync(eggPath) 160 | if (!eggPaths.includes(realpath)) { 161 | eggPaths.unshift(realpath) 162 | } 163 | } 164 | 165 | return eggPaths 166 | } 167 | 168 | loadFile (filepath, ...inject) { 169 | filepath = filepath && this.resolveModule(filepath) 170 | if (!filepath) { 171 | return null 172 | } 173 | 174 | // function(arg1, args, ...) {} 175 | if (inject.length === 0) inject = [this.app] 176 | 177 | let ret = this.requireFile(filepath) 178 | if (is.function(ret) && !is.class(ret)) { 179 | ret = ret(...inject) 180 | } 181 | return ret 182 | } 183 | 184 | requireFile (filepath) { 185 | const timingKey = `Require(${this[REQUIRE_COUNT]++}) ${utils.getResolvedFilename(filepath, this.options.baseDir)}` 186 | this.timing.start(timingKey) 187 | const ret = utils.loadFile(filepath) 188 | this.timing.end(timingKey) 189 | return ret 190 | } 191 | 192 | getLoadUnits () { 193 | if (this.dirs) { 194 | return this.dirs 195 | } 196 | 197 | const dirs = this.dirs = [] 198 | 199 | if (this.orderPlugins) { 200 | for (const plugin of this.orderPlugins) { 201 | dirs.push({ 202 | path: plugin.path, 203 | type: 'plugin' 204 | }) 205 | } 206 | } 207 | 208 | // framework or egg path 209 | for (const eggPath of this.eggPaths) { 210 | dirs.push({ 211 | path: eggPath, 212 | type: 'framework' 213 | }) 214 | } 215 | 216 | // application 217 | dirs.push({ 218 | path: this.options.baseDir, 219 | type: 'app' 220 | }) 221 | 222 | debug('Loaded dirs %j', dirs) 223 | return dirs 224 | } 225 | 226 | loadToApp (directory, property, opt) { 227 | const target = this.app[property] = {} 228 | opt = Object.assign({}, { 229 | directory, 230 | target, 231 | inject: this.app 232 | }, opt) 233 | 234 | const timingKey = `Load "${String(property)}" to Application` 235 | this.timing.start(timingKey) 236 | new FileLoader(opt).load() 237 | this.timing.end(timingKey) 238 | } 239 | 240 | loadToContext (directory, property, opt) { 241 | opt = Object.assign({}, { 242 | directory, 243 | property, 244 | inject: this.app 245 | }, opt) 246 | 247 | const timingKey = `Load "${String(property)}" to Context` 248 | this.timing.start(timingKey) 249 | new ContextLoader(opt).load() 250 | this.timing.end(timingKey) 251 | } 252 | 253 | get FileLoader () { 254 | return FileLoader 255 | } 256 | 257 | get ContextLoader () { 258 | return ContextLoader 259 | } 260 | 261 | getTypeFiles (filename) { 262 | const files = [`${filename}.default`] 263 | if (this.serverScope) files.push(`${filename}.${this.serverScope}`) 264 | if (this.serverEnv === 'default') return files 265 | 266 | files.push(`${filename}.${this.serverEnv}`) 267 | if (this.serverScope) files.push(`${filename}.${this.serverScope}_${this.serverEnv}`) 268 | return files 269 | } 270 | 271 | resolveModule (filepath) { 272 | let fullPath 273 | try { 274 | fullPath = require.resolve(filepath) 275 | } catch (e) { 276 | return undefined 277 | } 278 | 279 | if (process.env.EGG_TYPESCRIPT !== 'true' && fullPath.endsWith('.ts')) { 280 | return undefined 281 | } 282 | 283 | return fullPath 284 | } 285 | } 286 | 287 | const loaders = [ 288 | import('./mixin/plugin'), 289 | import('./mixin/config'), 290 | import('./mixin/extend'), 291 | import('./mixin/custom'), 292 | import('./mixin/service'), 293 | import('./mixin/middleware'), 294 | import('./mixin/controller'), 295 | import('./mixin/router'), 296 | import('./mixin/custom_loader') 297 | ] 298 | 299 | for (const loader of loaders) { 300 | Object.assign(EggLoader.prototype, loader) 301 | } 302 | -------------------------------------------------------------------------------- /demo/koa-router/router.ts: -------------------------------------------------------------------------------- 1 | import Layer from './Layer' 2 | import HttpError from 'http-errors' 3 | import compose from 'koa-compose' 4 | import methods from 'methods' 5 | const debug = require('debug')('koa-router') 6 | 7 | /** 8 | * @module koa-router 9 | */ 10 | 11 | export default function Router (this: any, opts?: any): void { 12 | if (!(this instanceof Router)) { 13 | return new Router(opts) 14 | } 15 | 16 | (this as any).opts = opts || {}; 17 | (this as any).methods = (this as any).opts.methods || [ 18 | 'HEAD', 19 | 'OPTIONS', 20 | 'GET', 21 | 'PUT', 22 | 'PATCH', 23 | 'POST', 24 | 'DELETE' 25 | ] 26 | 27 | ;(this as any).params = {} 28 | ;(this as any).stack = [] 29 | }; 30 | 31 | methods.forEach(function (method) { 32 | Router.prototype[method] = function (name, path, middleware) { 33 | if (typeof path === 'string' || path instanceof RegExp) { 34 | middleware = Array.prototype.slice.call(arguments, 2) 35 | } else { 36 | middleware = Array.prototype.slice.call(arguments, 1) 37 | path = name 38 | name = null 39 | } 40 | 41 | this.register(path, [method], middleware, { 42 | name: name 43 | }) 44 | 45 | return this 46 | } 47 | }) 48 | 49 | Router.prototype.del = Router.prototype.delete 50 | 51 | Router.prototype.use = function () { 52 | const router = this 53 | const middleware = Array.prototype.slice.call(arguments) 54 | let path 55 | 56 | // support array of paths 57 | if (Array.isArray(middleware[0]) && typeof middleware[0][0] === 'string') { 58 | middleware[0].forEach(function (p) { 59 | router.use.apply(router, [p].concat(middleware.slice(1))) 60 | }) 61 | 62 | return this 63 | } 64 | 65 | const hasPath = typeof middleware[0] === 'string' 66 | if (hasPath) { 67 | path = middleware.shift() 68 | } 69 | 70 | middleware.forEach(function (m) { 71 | if (m.router) { 72 | m.router.stack.forEach(function (nestedLayer) { 73 | if (path) nestedLayer.setPrefix(path) 74 | if (router.opts.prefix) nestedLayer.setPrefix(router.opts.prefix) 75 | router.stack.push(nestedLayer) 76 | }) 77 | 78 | if (router.params) { 79 | Object.keys(router.params).forEach(function (key) { 80 | m.router.param(key, router.params[key]) 81 | }) 82 | } 83 | } else { 84 | router.register(path || '(.*)', [], m, { end: false, ignoreCaptures: !hasPath }) 85 | } 86 | }) 87 | 88 | return this 89 | } 90 | 91 | Router.prototype.prefix = function (prefix) { 92 | prefix = prefix.replace(/\/$/, '') 93 | 94 | this.opts.prefix = prefix 95 | 96 | this.stack.forEach(function (route) { 97 | route.setPrefix(prefix) 98 | }) 99 | 100 | return this 101 | } 102 | 103 | Router.prototype.routes = Router.prototype.middleware = function () { 104 | const router = this 105 | 106 | const dispatch = function dispatch (ctx, next) { 107 | debug('%s %s', ctx.method, ctx.path) 108 | 109 | const path = router.opts.routerPath || ctx.routerPath || ctx.path 110 | const matched = router.match(path, ctx.method) 111 | 112 | if (ctx.matched) { 113 | ctx.matched.push.apply(ctx.matched, matched.path) 114 | } else { 115 | ctx.matched = matched.path 116 | } 117 | 118 | ctx.router = router 119 | 120 | if (!matched.route) return next() 121 | 122 | const matchedLayers = matched.pathAndMethod 123 | const mostSpecificLayer = matchedLayers[matchedLayers.length - 1] 124 | ctx._matchedRoute = mostSpecificLayer.path 125 | if (mostSpecificLayer.name) { 126 | ctx._matchedRouteName = mostSpecificLayer.name 127 | } 128 | 129 | const layerChain = matchedLayers.reduce(function (memo, layer) { 130 | memo.push(function (ctx, next) { 131 | ctx.captures = layer.captures(path, ctx.captures) 132 | ctx.params = layer.params(path, ctx.captures, ctx.params) 133 | ctx.routerName = layer.name 134 | return next() 135 | }) 136 | return memo.concat(layer.stack) 137 | }, []) 138 | 139 | return compose(layerChain)(ctx, next) 140 | }; 141 | 142 | (dispatch as any).router = this 143 | 144 | return dispatch 145 | } 146 | 147 | Router.prototype.allowedMethods = function (options) { 148 | options = options || {} 149 | const implemented = this.methods 150 | 151 | return function allowedMethods (ctx, next) { 152 | return next().then(function () { 153 | const allowed = {} 154 | 155 | if (!ctx.status || ctx.status === 404) { 156 | ctx.matched.forEach(function (route) { 157 | route.methods.forEach(function (method) { 158 | allowed[method] = method 159 | }) 160 | }) 161 | 162 | const allowedArr = Object.keys(allowed) 163 | 164 | if (!~implemented.indexOf(ctx.method)) { 165 | if (options.throw) { 166 | let notImplementedThrowable 167 | if (typeof options.notImplemented === 'function') { 168 | notImplementedThrowable = options.notImplemented() 169 | } else { 170 | notImplementedThrowable = new HttpError.NotImplemented() 171 | } 172 | throw notImplementedThrowable 173 | } else { 174 | ctx.status = 501 175 | ctx.set('Allow', allowedArr.join(', ')) 176 | } 177 | } else if (allowedArr.length) { 178 | if (ctx.method === 'OPTIONS') { 179 | ctx.status = 200 180 | ctx.body = '' 181 | ctx.set('Allow', allowedArr.join(', ')) 182 | } else if (!allowed[ctx.method]) { 183 | if (options.throw) { 184 | let notAllowedThrowable 185 | if (typeof options.methodNotAllowed === 'function') { 186 | notAllowedThrowable = options.methodNotAllowed() 187 | } else { 188 | notAllowedThrowable = new HttpError.MethodNotAllowed() 189 | } 190 | throw notAllowedThrowable 191 | } else { 192 | ctx.status = 405 193 | ctx.set('Allow', allowedArr.join(', ')) 194 | } 195 | } 196 | } 197 | } 198 | }) 199 | } 200 | } 201 | 202 | Router.prototype.all = function (name, path, middleware) { 203 | if (typeof path === 'string') { 204 | middleware = Array.prototype.slice.call(arguments, 2) 205 | } else { 206 | middleware = Array.prototype.slice.call(arguments, 1) 207 | path = name 208 | name = null 209 | } 210 | 211 | this.register(path, methods, middleware, { 212 | name: name 213 | }) 214 | 215 | return this 216 | } 217 | 218 | Router.prototype.redirect = function (source, destination, code) { 219 | // lookup source route by name 220 | if (source[0] !== '/') { 221 | source = this.url(source) 222 | } 223 | 224 | // lookup destination route by name 225 | if (destination[0] !== '/') { 226 | destination = this.url(destination) 227 | } 228 | 229 | return this.all(source, ctx => { 230 | ctx.redirect(destination) 231 | ctx.status = code || 301 232 | }) 233 | } 234 | 235 | Router.prototype.register = function (path, methods, middleware, opts) { 236 | opts = opts || {} 237 | 238 | const router = this 239 | const stack = this.stack 240 | 241 | // support array of paths 242 | if (Array.isArray(path)) { 243 | path.forEach(function (p) { 244 | // eslint-disable-next-line no-useless-call 245 | router.register.call(router, p, methods, middleware, opts) 246 | }) 247 | 248 | return this 249 | } 250 | 251 | // create route 252 | const route = new Layer(path, methods, middleware, { 253 | end: opts.end === false ? opts.end : true, 254 | name: opts.name, 255 | sensitive: opts.sensitive || this.opts.sensitive || false, 256 | strict: opts.strict || this.opts.strict || false, 257 | prefix: opts.prefix || this.opts.prefix || '', 258 | ignoreCaptures: opts.ignoreCaptures 259 | }) 260 | 261 | if (this.opts.prefix) { 262 | route.setPrefix(this.opts.prefix) 263 | } 264 | 265 | // add parameter middleware 266 | Object.keys(this.params).forEach(function (this: any, param) { 267 | route.param(param, this.params[param]) 268 | }, this) 269 | 270 | stack.push(route) 271 | 272 | return route 273 | } 274 | 275 | Router.prototype.route = function (name) { 276 | var routes = this.stack 277 | 278 | for (var len = routes.length, i = 0; i < len; i++) { 279 | if (routes[i].name && routes[i].name === name) { 280 | return routes[i] 281 | } 282 | } 283 | 284 | return false 285 | } 286 | 287 | Router.prototype.url = function (name, params) { 288 | var route = this.route(name) 289 | 290 | if (route) { 291 | var args = Array.prototype.slice.call(arguments, 1) 292 | return route.url.apply(route, args) 293 | } 294 | 295 | return new Error('No route found for name: ' + name) 296 | } 297 | 298 | Router.prototype.match = function (path, method) { 299 | var layers = this.stack 300 | var layer 301 | var matched = { 302 | path: [], 303 | pathAndMethod: [], 304 | route: false 305 | } 306 | 307 | for (var len = layers.length, i = 0; i < len; i++) { 308 | layer = layers[i] 309 | 310 | debug('test %s %s', layer.path, layer.regexp) 311 | 312 | if (layer.match(path)) { 313 | matched.path.push(layer) 314 | 315 | if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) { 316 | matched.pathAndMethod.push(layer) 317 | if (layer.methods.length) matched.route = true 318 | } 319 | } 320 | } 321 | 322 | return matched 323 | } 324 | 325 | Router.prototype.param = function (param, middleware) { 326 | this.params[param] = middleware 327 | this.stack.forEach(function (route) { 328 | route.param(param, middleware) 329 | }) 330 | return this 331 | } 332 | 333 | Router.url = function (path) { 334 | var args = Array.prototype.slice.call(arguments, 1) 335 | return Layer.prototype.url.apply({ path: path }, args) 336 | } 337 | -------------------------------------------------------------------------------- /packages/core/lib/loader/mixin/plugin.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs' 3 | const debug = require('debug')('egg-core:plugin') 4 | const sequencify = require('../../utils/sequencify') 5 | const loadFile = require('../../utils').loadFile 6 | 7 | export default { 8 | 9 | /** 10 | * Load config/plugin.js from {EggLoader#loadUnits} 11 | * 12 | * plugin.js is written below 13 | * 14 | * ```js 15 | * { 16 | * 'xxx-client': { 17 | * enable: true, 18 | * package: 'xxx-client', 19 | * dep: [], 20 | * env: [], 21 | * }, 22 | * // short hand 23 | * 'rds': false, 24 | * 'depd': { 25 | * enable: true, 26 | * path: 'path/to/depd' 27 | * } 28 | * } 29 | * ``` 30 | * 31 | * If the plugin has path, Loader will find the module from it. 32 | * 33 | * Otherwise Loader will lookup follow the order by packageName 34 | * 35 | * 1. $APP_BASE/node_modules/${package} 36 | * 2. $EGG_BASE/node_modules/${package} 37 | * 38 | * You can call `loader.plugins` that retrieve enabled plugins. 39 | * 40 | * ```js 41 | * loader.plugins['xxx-client'] = { 42 | * name: 'xxx-client', // the plugin name, it can be used in `dep` 43 | * package: 'xxx-client', // the package name of plugin 44 | * enable: true, // whether enabled 45 | * path: 'path/to/xxx-client', // the directory of the plugin package 46 | * dep: [], // the dependent plugins, you can use the plugin name 47 | * env: [ 'local', 'unittest' ], // specify the serverEnv that only enable the plugin in it 48 | * } 49 | * ``` 50 | * 51 | * `loader.allPlugins` can be used when retrieve all plugins. 52 | * @function EggLoader#loadPlugin 53 | * @since 1.0.0 54 | */ 55 | loadPlugin () { 56 | const _that = this as any 57 | 58 | _that.timing.start('Load Plugin') 59 | 60 | // loader plugins from application 61 | const appPlugins = this.readPluginConfigs(path.join(_that.options.baseDir, 'config/plugin.default')) 62 | debug('Loaded app plugins: %j', Object.keys(appPlugins)) 63 | 64 | // loader plugins from framework 65 | const eggPluginConfigPaths = _that.eggPaths.map(eggPath => path.join(eggPath, 'config/plugin.default')) 66 | const eggPlugins = this.readPluginConfigs(eggPluginConfigPaths) 67 | debug('Loaded egg plugins: %j', Object.keys(eggPlugins)) 68 | 69 | // loader plugins from process.env.EGG_PLUGINS 70 | let customPlugins 71 | if (process.env.EGG_PLUGINS) { 72 | try { 73 | customPlugins = JSON.parse(process.env.EGG_PLUGINS) 74 | } catch (e) { 75 | debug('parse EGG_PLUGINS failed, %s', e) 76 | } 77 | } 78 | 79 | // loader plugins from options.plugins 80 | if (_that.options.plugins) { 81 | customPlugins = Object.assign({}, customPlugins, _that.options.plugins) 82 | } 83 | 84 | if (customPlugins) { 85 | for (const name in customPlugins) { 86 | _that.normalizePluginConfig(customPlugins, name) 87 | } 88 | debug('Loaded custom plugins: %j', Object.keys(customPlugins)) 89 | } 90 | 91 | _that.allPlugins = {} 92 | _that.appPlugins = appPlugins 93 | _that.customPlugins = customPlugins 94 | _that.eggPlugins = eggPlugins 95 | 96 | this._extendPlugins(_that.allPlugins, eggPlugins) 97 | this._extendPlugins(_that.allPlugins, appPlugins) 98 | this._extendPlugins(_that.allPlugins, customPlugins) 99 | 100 | const enabledPluginNames = [] // enabled plugins that configured explicitly 101 | const plugins = {} 102 | const env = _that.serverEnv 103 | for (const name in _that.allPlugins) { 104 | const plugin = _that.allPlugins[name] 105 | 106 | // resolve the real plugin.path based on plugin or package 107 | plugin.path = this.getPluginPath(plugin) 108 | 109 | // read plugin information from ${plugin.path}/package.json 110 | this.mergePluginConfig(plugin) 111 | 112 | // disable the plugin that not match the serverEnv 113 | if (env && plugin.env.length && !plugin.env.includes(env)) { 114 | _that.options.logger.info('Plugin %s is disabled by env unmatched, require env(%s) but got env is %s', name, plugin.env, env) 115 | plugin.enable = false 116 | continue 117 | } 118 | 119 | plugins[name] = plugin 120 | if (plugin.enable) { 121 | enabledPluginNames.push(name) 122 | } 123 | } 124 | 125 | // retrieve the ordered plugins 126 | _that.orderPlugins = this.getOrderPlugins(plugins, enabledPluginNames, appPlugins) 127 | 128 | const enablePlugins = {} 129 | for (const plugin of _that.orderPlugins) { 130 | enablePlugins[plugin.name] = plugin 131 | } 132 | debug('Loaded plugins: %j', Object.keys(enablePlugins)) 133 | 134 | /** 135 | * Retrieve enabled plugins 136 | * @member {Object} EggLoader#plugins 137 | * @since 1.0.0 138 | */ 139 | _that.plugins = enablePlugins 140 | 141 | _that.timing.end('Load Plugin') 142 | }, 143 | 144 | /* 145 | * Read plugin.js from multiple directory 146 | */ 147 | readPluginConfigs (configPaths) { 148 | if (!Array.isArray(configPaths)) { 149 | configPaths = [configPaths] 150 | } 151 | const _that = this as any 152 | // Get all plugin configurations 153 | // plugin.default.js 154 | // plugin.${scope}.js 155 | // plugin.${env}.js 156 | // plugin.${scope}_${env}.js 157 | const newConfigPaths = [] 158 | for (const filename of _that.getTypeFiles('plugin')) { 159 | for (let configPath of configPaths) { 160 | configPath = path.join(path.dirname(configPath), filename) 161 | newConfigPaths.push(configPath) 162 | } 163 | } 164 | 165 | const plugins = {} 166 | for (const configPath of newConfigPaths) { 167 | let filepath = _that.resolveModule(configPath) 168 | 169 | // let plugin.js compatible 170 | if (configPath.endsWith('plugin.default') && !filepath) { 171 | filepath = _that.resolveModule(configPath.replace(/plugin\.default$/, 'plugin')) 172 | } 173 | 174 | if (!filepath) { 175 | continue 176 | } 177 | 178 | const config = loadFile(filepath) 179 | 180 | for (const name in config) { 181 | this.normalizePluginConfig(config, name, filepath) 182 | } 183 | 184 | this._extendPlugins(plugins, config) 185 | } 186 | 187 | return plugins 188 | }, 189 | 190 | normalizePluginConfig (plugins, name, configPath) { 191 | const plugin = plugins[name] 192 | 193 | // plugin_name: false 194 | if (typeof plugin === 'boolean') { 195 | plugins[name] = { 196 | name, 197 | enable: plugin, 198 | dependencies: [], 199 | optionalDependencies: [], 200 | env: [], 201 | from: configPath 202 | } 203 | return 204 | } 205 | 206 | if (!('enable' in plugin)) { 207 | plugin.enable = true 208 | } 209 | plugin.name = name 210 | plugin.dependencies = plugin.dependencies || [] 211 | plugin.optionalDependencies = plugin.optionalDependencies || [] 212 | plugin.env = plugin.env || [] 213 | plugin.from = configPath 214 | depCompatible(plugin) 215 | }, 216 | 217 | // Read plugin information from package.json and merge 218 | // { 219 | // eggPlugin: { 220 | // "name": "", plugin name, must be same as name in config/plugin.js 221 | // "dep": [], dependent plugins 222 | // "env": "" env 223 | // } 224 | // } 225 | mergePluginConfig (plugin) { 226 | let pkg 227 | let config 228 | const _that = this as any 229 | const pluginPackage = path.join(plugin.path, 'package.json') 230 | if (fs.existsSync(pluginPackage)) { 231 | pkg = require(pluginPackage) 232 | config = pkg.eggPlugin 233 | if (pkg.version) { 234 | plugin.version = pkg.version 235 | } 236 | } 237 | 238 | const logger = _that.options.logger 239 | if (!config) { 240 | logger.warn(`[egg:loader] pkg.eggPlugin is missing in ${pluginPackage}`) 241 | return 242 | } 243 | 244 | if (config.name && config.name !== plugin.name) { 245 | // pluginName is configured in config/plugin.js 246 | // pluginConfigName is pkg.eggPlugin.name 247 | logger.warn(`[egg:loader] pluginName(${plugin.name}) is different from pluginConfigName(${config.name})`) 248 | } 249 | 250 | // dep compatible 251 | depCompatible(config) 252 | 253 | for (const key of ['dependencies', 'optionalDependencies', 'env']) { 254 | if (!plugin[key].length && Array.isArray(config[key])) { 255 | plugin[key] = config[key] 256 | } 257 | } 258 | }, 259 | 260 | getOrderPlugins (allPlugins, enabledPluginNames, appPlugins) { 261 | // no plugins enabled 262 | if (!enabledPluginNames.length) { 263 | return [] 264 | } 265 | const _that = this as any 266 | const result = sequencify(allPlugins, enabledPluginNames) 267 | debug('Got plugins %j after sequencify', result) 268 | 269 | // catch error when result.sequence is empty 270 | if (!result.sequence.length) { 271 | const err = new Error(`sequencify plugins has problem, missing: [${result.missingTasks}], recursive: [${result.recursiveDependencies}]`) 272 | // find plugins which is required by the missing plugin 273 | for (const missName of result.missingTasks) { 274 | const requires = [] 275 | for (const name in allPlugins) { 276 | if (allPlugins[name].dependencies.includes(missName)) { 277 | requires.push(name) 278 | } 279 | } 280 | err.message += `\n\t>> Plugin [${missName}] is disabled or missed, but is required by [${requires}]` 281 | } 282 | 283 | err.name = 'PluginSequencifyError' 284 | throw err 285 | } 286 | 287 | // log the plugins that be enabled implicitly 288 | const implicitEnabledPlugins = [] 289 | const requireMap = {} 290 | result.sequence.forEach(name => { 291 | for (const depName of allPlugins[name].dependencies) { 292 | if (!requireMap[depName]) { 293 | requireMap[depName] = [] 294 | } 295 | requireMap[depName].push(name) 296 | } 297 | 298 | if (!allPlugins[name].enable) { 299 | implicitEnabledPlugins.push(name) 300 | allPlugins[name].enable = true 301 | } 302 | }) 303 | 304 | // Following plugins will be enabled implicitly. 305 | // - configclient required by [hsfclient] 306 | // - eagleeye required by [hsfclient] 307 | // - diamond required by [hsfclient] 308 | if (implicitEnabledPlugins.length) { 309 | let message = implicitEnabledPlugins 310 | .map(name => ` - ${name} required by [${requireMap[name]}]`) 311 | .join('\n') 312 | _that.options.logger.info(`Following plugins will be enabled implicitly.\n${message}`) 313 | 314 | // should warn when the plugin is disabled by app 315 | const disabledPlugins = implicitEnabledPlugins.filter(name => appPlugins[name] && appPlugins[name].enable === false) 316 | if (disabledPlugins.length) { 317 | message = disabledPlugins 318 | .map(name => ` - ${name} required by [${requireMap[name]}]`) 319 | .join('\n') 320 | _that.options.logger.warn(`Following plugins will be enabled implicitly that is disabled by application.\n${message}`) 321 | } 322 | } 323 | 324 | return result.sequence.map(name => allPlugins[name]) 325 | }, 326 | 327 | // Get the real plugin path 328 | getPluginPath (plugin) { 329 | if (plugin.path) { 330 | return plugin.path 331 | } 332 | const _that = this as any 333 | const name = plugin.package || plugin.name 334 | const lookupDirs = [] 335 | 336 | // 尝试在以下目录找到匹配的插件 337 | // -> {APP_PATH}/node_modules 338 | // -> {EGG_PATH}/node_modules 339 | // -> $CWD/node_modules 340 | lookupDirs.push(path.join(_that.options.baseDir, 'node_modules')) 341 | 342 | // 到 egg 中查找,优先从外往里查找 343 | for (let i = _that.eggPaths.length - 1; i >= 0; i--) { 344 | const eggPath = _that.eggPaths[i] 345 | lookupDirs.push(path.join(eggPath, 'node_modules')) 346 | } 347 | 348 | // should find the $cwd/node_modules when test the plugins under npm3 349 | lookupDirs.push(path.join(process.cwd(), 'node_modules')) 350 | 351 | for (let dir of lookupDirs) { 352 | dir = path.join(dir, name) 353 | if (fs.existsSync(dir)) { 354 | return fs.realpathSync(dir) 355 | } 356 | } 357 | 358 | throw new Error(`Can not find plugin ${name} in "${lookupDirs.join(', ')}"`) 359 | }, 360 | 361 | _extendPlugins (target, plugins) { 362 | if (!plugins) { 363 | return 364 | } 365 | for (const name in plugins) { 366 | const plugin = plugins[name] 367 | let targetPlugin = target[name] 368 | if (!targetPlugin) { 369 | targetPlugin = target[name] = {} 370 | } 371 | if (targetPlugin.package && targetPlugin.package === plugin.package) { 372 | (this as any).options.logger.warn('plugin %s has been defined that is %j, but you define again in %s', 373 | name, targetPlugin, plugin.from) 374 | } 375 | if (plugin.path || plugin.package) { 376 | delete targetPlugin.path 377 | delete targetPlugin.package 378 | } 379 | for (const prop in plugin) { 380 | if (plugin[prop] === undefined) { 381 | continue 382 | } 383 | if (targetPlugin[prop] && Array.isArray(plugin[prop]) && !plugin[prop].length) { 384 | continue 385 | } 386 | targetPlugin[prop] = plugin[prop] 387 | } 388 | } 389 | } 390 | 391 | } 392 | 393 | function depCompatible (plugin) { 394 | if (plugin.dep && !(Array.isArray(plugin.dependencies) && plugin.dependencies.length)) { 395 | plugin.dependencies = plugin.dep 396 | delete plugin.dep 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /demo/koa/extend/response.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // eslint-disable-next-line no-unused-vars 3 | import KoaApplication = require('koa'); 4 | 5 | const contentDisposition = require('content-disposition') 6 | const ensureErrorHandler = require('error-inject') 7 | const getType = require('cache-content-type') 8 | const onFinish = require('on-finished') 9 | const escape = require('escape-html') 10 | const typeis = require('type-is').is 11 | const statuses = require('statuses') 12 | const destroy = require('destroy') 13 | const assert = require('assert') 14 | const extname = require('path').extname 15 | const vary = require('vary') 16 | const only = require('only') 17 | const util = require('util') 18 | const encodeUrl = require('encodeurl') 19 | const Stream = require('stream') 20 | 21 | /** 22 | * Prototype. 23 | */ 24 | 25 | export default { 26 | 27 | /** 28 | * Return the request socket. 29 | * 30 | * @return {Connection} 31 | * @api public 32 | */ 33 | 34 | get socket () { 35 | return this.res.socket 36 | }, 37 | 38 | /** 39 | * Return response header. 40 | * 41 | * @return {Object} 42 | * @api public 43 | */ 44 | 45 | get header () { 46 | const { res } = this 47 | return typeof res.getHeaders === 'function' 48 | ? res.getHeaders() 49 | : res._headers || {} // Node < 7.7 50 | }, 51 | 52 | /** 53 | * Return response header, alias as response.header 54 | * 55 | * @return {Object} 56 | * @api public 57 | */ 58 | 59 | get headers () { 60 | return this.header 61 | }, 62 | 63 | /** 64 | * Get response status code. 65 | * 66 | * @return {Number} 67 | * @api public 68 | */ 69 | 70 | get status () { 71 | return this.res.statusCode 72 | }, 73 | 74 | /** 75 | * Set response status code. 76 | * 77 | * @param {Number} code 78 | * @api public 79 | */ 80 | 81 | set status (code) { 82 | if (this.headerSent) return 83 | 84 | assert(Number.isInteger(code), 'status code must be a number') 85 | assert(code >= 100 && code <= 999, `invalid status code: ${code}`) 86 | this._explicitStatus = true 87 | this.res.statusCode = code 88 | if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code] 89 | if (this.body && statuses.empty[code]) this.body = null 90 | }, 91 | 92 | /** 93 | * Get response status message 94 | * 95 | * @return {String} 96 | * @api public 97 | */ 98 | 99 | get message () { 100 | return this.res.statusMessage || statuses[this.status] 101 | }, 102 | 103 | /** 104 | * Set response status message 105 | * 106 | * @param {String} msg 107 | * @api public 108 | */ 109 | 110 | set message (msg) { 111 | this.res.statusMessage = msg 112 | }, 113 | 114 | /** 115 | * Get response body. 116 | * 117 | * @return {Mixed} 118 | * @api public 119 | */ 120 | 121 | get body () { 122 | return this._body 123 | }, 124 | 125 | /** 126 | * Set response body. 127 | * 128 | * @param {String|Buffer|Object|Stream} val 129 | * @api public 130 | */ 131 | 132 | set body (val) { 133 | const original = this._body 134 | this._body = val 135 | 136 | // no content 137 | if (val == null) { 138 | if (!statuses.empty[this.status]) this.status = 204 139 | if (val === null) this._explicitNullBody = true 140 | this.remove('Content-Type') 141 | this.remove('Content-Length') 142 | this.remove('Transfer-Encoding') 143 | return 144 | } 145 | 146 | // set the status 147 | if (!this._explicitStatus) this.status = 200 148 | 149 | // set the content-type only if not yet set 150 | const setType = !this.has('Content-Type') 151 | 152 | // string 153 | if (typeof val === 'string') { 154 | if (setType) this.type = /^\s* this.ctx.onerror(err)) 170 | 171 | // overwriting 172 | if (original != null && original !== val) this.remove('Content-Length') 173 | 174 | if (setType) this.type = 'bin' 175 | return 176 | } 177 | 178 | // json 179 | this.remove('Content-Length') 180 | this.type = 'json' 181 | }, 182 | 183 | /** 184 | * Set Content-Length field to `n`. 185 | * 186 | * @param {Number} n 187 | * @api public 188 | */ 189 | 190 | set length (n) { 191 | this.set('Content-Length', n) 192 | }, 193 | 194 | /** 195 | * Return parsed response Content-Length when present. 196 | * 197 | * @return {Number} 198 | * @api public 199 | */ 200 | 201 | get length () { 202 | if (this.has('Content-Length')) { 203 | return parseInt(this.get('Content-Length'), 10) || 0 204 | } 205 | 206 | const { body } = this 207 | if (!body || body instanceof Stream) return undefined 208 | if (typeof body === 'string') return Buffer.byteLength(body) 209 | if (Buffer.isBuffer(body)) return body.length 210 | return Buffer.byteLength(JSON.stringify(body)) 211 | }, 212 | 213 | /** 214 | * Check if a header has been written to the socket. 215 | * 216 | * @return {Boolean} 217 | * @api public 218 | */ 219 | 220 | get headerSent () { 221 | return this.res.headersSent 222 | }, 223 | 224 | /** 225 | * Vary on `field`. 226 | * 227 | * @param {String} field 228 | * @api public 229 | */ 230 | 231 | vary (field) { 232 | if (this.headerSent) return 233 | 234 | vary(this.res, field) 235 | }, 236 | 237 | /** 238 | * Perform a 302 redirect to `url`. 239 | * 240 | * The string "back" is special-cased 241 | * to provide Referrer support, when Referrer 242 | * is not present `alt` or "/" is used. 243 | * 244 | * Examples: 245 | * 246 | * this.redirect('back'); 247 | * this.redirect('back', '/index.html'); 248 | * this.redirect('/login'); 249 | * this.redirect('http://google.com'); 250 | * 251 | * @param {String} url 252 | * @param {String} [alt] 253 | * @api public 254 | */ 255 | 256 | redirect (url, alt) { 257 | // location 258 | if (url === 'back') url = this.ctx.get('Referrer') || alt || '/' 259 | this.set('Location', encodeUrl(url)) 260 | 261 | // status 262 | if (!statuses.redirect[this.status]) this.status = 302 263 | 264 | // html 265 | if (this.ctx.accepts('html')) { 266 | url = escape(url) 267 | this.type = 'text/html; charset=utf-8' 268 | this.body = `Redirecting to ${url}.` 269 | return 270 | } 271 | 272 | // text 273 | this.type = 'text/plain; charset=utf-8' 274 | this.body = `Redirecting to ${url}.` 275 | }, 276 | 277 | /** 278 | * Set Content-Disposition header to "attachment" with optional `filename`. 279 | * 280 | * @param {String} filename 281 | * @api public 282 | */ 283 | 284 | attachment (filename, options) { 285 | if (filename) this.type = extname(filename) 286 | this.set('Content-Disposition', contentDisposition(filename, options)) 287 | }, 288 | 289 | /** 290 | * Set Content-Type response header with `type` through `mime.lookup()` 291 | * when it does not contain a charset. 292 | * 293 | * Examples: 294 | * 295 | * this.type = '.html'; 296 | * this.type = 'html'; 297 | * this.type = 'json'; 298 | * this.type = 'application/json'; 299 | * this.type = 'png'; 300 | * 301 | * @param {String} type 302 | * @api public 303 | */ 304 | 305 | set type (type) { 306 | type = getType(type) 307 | if (type) { 308 | this.set('Content-Type', type) 309 | } else { 310 | this.remove('Content-Type') 311 | } 312 | }, 313 | 314 | /** 315 | * Set the Last-Modified date using a string or a Date. 316 | * 317 | * this.response.lastModified = new Date(); 318 | * this.response.lastModified = '2013-09-13'; 319 | * 320 | * @param {String|Date} type 321 | * @api public 322 | */ 323 | 324 | set lastModified (val) { 325 | if (typeof val === 'string') val = new Date(val) 326 | this.set('Last-Modified', val.toUTCString()) 327 | }, 328 | 329 | /** 330 | * Get the Last-Modified date in Date form, if it exists. 331 | * 332 | * @return {Date} 333 | * @api public 334 | */ 335 | 336 | get lastModified () { 337 | const date = this.get('last-modified') 338 | if (date) return new Date(date) 339 | }, 340 | 341 | /** 342 | * Set the ETag of a response. 343 | * This will normalize the quotes if necessary. 344 | * 345 | * this.response.etag = 'md5hashsum'; 346 | * this.response.etag = '"md5hashsum"'; 347 | * this.response.etag = 'W/"123456789"'; 348 | * 349 | * @param {String} etag 350 | * @api public 351 | */ 352 | 353 | set etag (val) { 354 | if (!/^(W\/)?"/.test(val)) val = `"${val}"` 355 | this.set('ETag', val) 356 | }, 357 | 358 | /** 359 | * Get the ETag of a response. 360 | * 361 | * @return {String} 362 | * @api public 363 | */ 364 | 365 | get etag () { 366 | return this.get('ETag') 367 | }, 368 | 369 | /** 370 | * Return the response mime type void of 371 | * parameters such as "charset". 372 | * 373 | * @return {String} 374 | * @api public 375 | */ 376 | 377 | get type () { 378 | const type = this.get('Content-Type') 379 | if (!type) return '' 380 | return type.split(';', 1)[0] 381 | }, 382 | 383 | /** 384 | * Check whether the response is one of the listed types. 385 | * Pretty much the same as `this.request.is()`. 386 | * 387 | * @param {String|String[]} [type] 388 | * @param {String[]} [types] 389 | * @return {String|false} 390 | * @api public 391 | */ 392 | 393 | is (type, ...types) { 394 | return typeis(this.type, type, ...types) 395 | }, 396 | 397 | /** 398 | * Return response header. 399 | * 400 | * Examples: 401 | * 402 | * this.get('Content-Type'); 403 | * // => "text/plain" 404 | * 405 | * this.get('content-type'); 406 | * // => "text/plain" 407 | * 408 | * @param {String} field 409 | * @return {String} 410 | * @api public 411 | */ 412 | 413 | get (field) { 414 | return this.header[field.toLowerCase()] || '' 415 | }, 416 | 417 | /** 418 | * Returns true if the header identified by name is currently set in the outgoing headers. 419 | * The header name matching is case-insensitive. 420 | * 421 | * Examples: 422 | * 423 | * this.has('Content-Type'); 424 | * // => true 425 | * 426 | * this.get('content-type'); 427 | * // => true 428 | * 429 | * @param {String} field 430 | * @return {boolean} 431 | * @api public 432 | */ 433 | has (field) { 434 | return typeof this.res.hasHeader === 'function' 435 | ? this.res.hasHeader(field) 436 | // Node < 7.7 437 | : field.toLowerCase() in this.headers 438 | }, 439 | 440 | /** 441 | * Set header `field` to `val`, or pass 442 | * an object of header fields. 443 | * 444 | * Examples: 445 | * 446 | * this.set('Foo', ['bar', 'baz']); 447 | * this.set('Accept', 'application/json'); 448 | * this.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' }); 449 | * 450 | * @param {String|Object|Array} field 451 | * @param {String} val 452 | * @api public 453 | */ 454 | 455 | set (field, val) { 456 | if (this.headerSent) return 457 | 458 | if (arguments.length === 2) { 459 | if (Array.isArray(val)) val = val.map(v => typeof v === 'string' ? v : String(v)) 460 | else if (typeof val !== 'string') val = String(val) 461 | this.res.setHeader(field, val) 462 | } else { 463 | for (const key in field) { 464 | this.set(key, field[key]) 465 | } 466 | } 467 | }, 468 | 469 | /** 470 | * Append additional header `field` with value `val`. 471 | * 472 | * Examples: 473 | * 474 | * ``` 475 | * this.append('Link', ['', '']); 476 | * this.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly'); 477 | * this.append('Warning', '199 Miscellaneous warning'); 478 | * ``` 479 | * 480 | * @param {String} field 481 | * @param {String|Array} val 482 | * @api public 483 | */ 484 | 485 | append (field, val) { 486 | const prev = this.get(field) 487 | 488 | if (prev) { 489 | val = Array.isArray(prev) 490 | ? prev.concat(val) 491 | : [prev].concat(val) 492 | } 493 | 494 | return this.set(field, val) 495 | }, 496 | 497 | /** 498 | * Remove header `field`. 499 | * 500 | * @param {String} name 501 | * @api public 502 | */ 503 | 504 | remove (field) { 505 | if (this.headerSent) return 506 | 507 | this.res.removeHeader(field) 508 | }, 509 | 510 | /** 511 | * Checks if the request is writable. 512 | * Tests for the existence of the socket 513 | * as node sometimes does not set it. 514 | * 515 | * @return {Boolean} 516 | * @api private 517 | */ 518 | 519 | get writable () { 520 | // can't write any more after response finished 521 | // response.writableEnded is available since Node > 12.9 522 | // https://nodejs.org/api/http.html#http_response_writableended 523 | // response.finished is undocumented feature of previous Node versions 524 | // https://stackoverflow.com/questions/16254385/undocumented-response-finished-in-node-js 525 | if (this.res.writableEnded || this.res.finished) return false 526 | 527 | const socket = this.res.socket 528 | // There are already pending outgoing res, but still writable 529 | // https://github.com/nodejs/node/blob/v4.4.7/lib/_http_server.js#L486 530 | if (!socket) return true 531 | return socket.writable 532 | }, 533 | 534 | /** 535 | * Inspect implementation. 536 | * 537 | * @return {Object} 538 | * @api public 539 | */ 540 | 541 | inspect () { 542 | if (!this.res) return 543 | const o = this.toJSON() 544 | o.body = this.body 545 | return o 546 | }, 547 | 548 | /** 549 | * Return JSON representation. 550 | * 551 | * @return {Object} 552 | * @api public 553 | */ 554 | 555 | toJSON () { 556 | return only(this, [ 557 | 'status', 558 | 'message', 559 | 'header' 560 | ]) 561 | }, 562 | 563 | /** 564 | * Flush any set headers, and begin the body 565 | */ 566 | flushHeaders () { 567 | this.res.flushHeaders() 568 | } 569 | } 570 | 571 | /** 572 | * Custom inspection implementation for newer Node.js versions. 573 | * 574 | * @return {Object} 575 | * @api public 576 | */ 577 | if (util.inspect.custom) { 578 | module.exports[util.inspect.custom] = module.exports.inspect 579 | } 580 | -------------------------------------------------------------------------------- /demo/koa/extend/request.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // eslint-disable-next-line no-unused-vars 3 | import KoaApplication = require('koa'); 4 | 5 | const URL = require('url').URL 6 | const net = require('net') 7 | const accepts = require('accepts') 8 | const contentType = require('content-type') 9 | const stringify = require('url').format 10 | const parse = require('parseurl') 11 | const qs = require('querystring') 12 | const typeis = require('type-is') 13 | const fresh = require('fresh') 14 | const only = require('only') 15 | const util = require('util') 16 | 17 | const IP = Symbol('context#ip') 18 | 19 | /** 20 | * Prototype. 21 | */ 22 | 23 | export default { 24 | 25 | /** 26 | * Return request header. 27 | * 28 | * @return {Object} 29 | * @api public 30 | */ 31 | 32 | get header () { 33 | return this.req.headers 34 | }, 35 | 36 | /** 37 | * Set request header. 38 | * 39 | * @api public 40 | */ 41 | 42 | set header (val) { 43 | this.req.headers = val 44 | }, 45 | 46 | /** 47 | * Return request header, alias as request.header 48 | * 49 | * @return {Object} 50 | * @api public 51 | */ 52 | 53 | get headers () { 54 | return this.req.headers 55 | }, 56 | 57 | /** 58 | * Set request header, alias as request.header 59 | * 60 | * @api public 61 | */ 62 | 63 | set headers (val) { 64 | this.req.headers = val 65 | }, 66 | 67 | /** 68 | * Get request URL. 69 | * 70 | * @return {String} 71 | * @api public 72 | */ 73 | 74 | get url () { 75 | return this.req.url 76 | }, 77 | 78 | /** 79 | * Set request URL. 80 | * 81 | * @api public 82 | */ 83 | 84 | set url (val) { 85 | this.req.url = val 86 | }, 87 | 88 | /** 89 | * Get origin of URL. 90 | * 91 | * @return {String} 92 | * @api public 93 | */ 94 | 95 | get origin () { 96 | return `${this.protocol}://${this.host}` 97 | }, 98 | 99 | /** 100 | * Get full request URL. 101 | * 102 | * @return {String} 103 | * @api public 104 | */ 105 | 106 | get href () { 107 | // support: `GET http://example.com/foo` 108 | if (/^https?:\/\//i.test(this.originalUrl)) return this.originalUrl 109 | return this.origin + this.originalUrl 110 | }, 111 | 112 | /** 113 | * Get request method. 114 | * 115 | * @return {String} 116 | * @api public 117 | */ 118 | 119 | get method () { 120 | return this.req.method 121 | }, 122 | 123 | /** 124 | * Set request method. 125 | * 126 | * @param {String} val 127 | * @api public 128 | */ 129 | 130 | set method (val) { 131 | this.req.method = val 132 | }, 133 | 134 | /** 135 | * Get request pathname. 136 | * 137 | * @return {String} 138 | * @api public 139 | */ 140 | 141 | get path () { 142 | return parse(this.req).pathname 143 | }, 144 | 145 | /** 146 | * Set pathname, retaining the query-string when present. 147 | * 148 | * @param {String} path 149 | * @api public 150 | */ 151 | 152 | set path (path) { 153 | const url = parse(this.req) 154 | if (url.pathname === path) return 155 | 156 | url.pathname = path 157 | url.path = null 158 | 159 | this.url = stringify(url) 160 | }, 161 | 162 | /** 163 | * Get parsed query-string. 164 | * 165 | * @return {Object} 166 | * @api public 167 | */ 168 | 169 | get query () { 170 | const str = this.querystring 171 | const c = this._querycache = this._querycache || {} 172 | return c[str] || (c[str] = qs.parse(str)) 173 | }, 174 | 175 | /** 176 | * Set query-string as an object. 177 | * 178 | * @param {Object} obj 179 | * @api public 180 | */ 181 | 182 | set query (obj) { 183 | this.querystring = qs.stringify(obj) 184 | }, 185 | 186 | /** 187 | * Get query string. 188 | * 189 | * @return {String} 190 | * @api public 191 | */ 192 | 193 | get querystring () { 194 | if (!this.req) return '' 195 | return parse(this.req).query || '' 196 | }, 197 | 198 | /** 199 | * Set querystring. 200 | * 201 | * @param {String} str 202 | * @api public 203 | */ 204 | 205 | set querystring (str) { 206 | const url = parse(this.req) 207 | if (url.search === `?${str}`) return 208 | 209 | url.search = str 210 | url.path = null 211 | 212 | this.url = stringify(url) 213 | }, 214 | 215 | /** 216 | * Get the search string. Same as the querystring 217 | * except it includes the leading ?. 218 | * 219 | * @return {String} 220 | * @api public 221 | */ 222 | 223 | get search () { 224 | if (!this.querystring) return '' 225 | return `?${this.querystring}` 226 | }, 227 | 228 | /** 229 | * Set the search string. Same as 230 | * request.querystring= but included for ubiquity. 231 | * 232 | * @param {String} str 233 | * @api public 234 | */ 235 | 236 | set search (str) { 237 | this.querystring = str 238 | }, 239 | 240 | /** 241 | * Parse the "Host" header field host 242 | * and support X-Forwarded-Host when a 243 | * proxy is enabled. 244 | * 245 | * @return {String} hostname:port 246 | * @api public 247 | */ 248 | 249 | get host () { 250 | const proxy = this.app.proxy 251 | let host = proxy && this.get('X-Forwarded-Host') 252 | if (!host) { 253 | if (this.req.httpVersionMajor >= 2) host = this.get(':authority') 254 | if (!host) host = this.get('Host') 255 | } 256 | if (!host) return '' 257 | return host.split(/\s*,\s*/, 1)[0] 258 | }, 259 | 260 | /** 261 | * Parse the "Host" header field hostname 262 | * and support X-Forwarded-Host when a 263 | * proxy is enabled. 264 | * 265 | * @return {String} hostname 266 | * @api public 267 | */ 268 | 269 | get hostname () { 270 | const host = this.host 271 | if (!host) return '' 272 | if (host[0] === '[') return this.URL.hostname || '' // IPv6 273 | return host.split(':', 1)[0] 274 | }, 275 | 276 | /** 277 | * Get WHATWG parsed URL. 278 | * Lazily memoized. 279 | * 280 | * @return {URL|Object} 281 | * @api public 282 | */ 283 | 284 | get URL () { 285 | /* istanbul ignore else */ 286 | if (!this.memoizedURL) { 287 | const originalUrl = this.originalUrl || '' // avoid undefined in template string 288 | try { 289 | this.memoizedURL = new URL(`${this.origin}${originalUrl}`) 290 | } catch (err) { 291 | this.memoizedURL = Object.create(null) 292 | } 293 | } 294 | return this.memoizedURL 295 | }, 296 | 297 | /** 298 | * Check if the request is fresh, aka 299 | * Last-Modified and/or the ETag 300 | * still match. 301 | * 302 | * @return {Boolean} 303 | * @api public 304 | */ 305 | 306 | get fresh () { 307 | const method = this.method 308 | const s = this.ctx.status 309 | 310 | // GET or HEAD for weak freshness validation only 311 | if (method !== 'GET' && method !== 'HEAD') return false 312 | 313 | // 2xx or 304 as per rfc2616 14.26 314 | if ((s >= 200 && s < 300) || s === 304) { 315 | return fresh(this.header, this.response.header) 316 | } 317 | 318 | return false 319 | }, 320 | 321 | /** 322 | * Check if the request is stale, aka 323 | * "Last-Modified" and / or the "ETag" for the 324 | * resource has changed. 325 | * 326 | * @return {Boolean} 327 | * @api public 328 | */ 329 | 330 | get stale () { 331 | return !this.fresh 332 | }, 333 | 334 | /** 335 | * Check if the request is idempotent. 336 | * 337 | * @return {Boolean} 338 | * @api public 339 | */ 340 | 341 | get idempotent () { 342 | const methods = ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE'] 343 | return !!~methods.indexOf(this.method) 344 | }, 345 | 346 | /** 347 | * Return the request socket. 348 | * 349 | * @return {Connection} 350 | * @api public 351 | */ 352 | 353 | get socket () { 354 | return this.req.socket 355 | }, 356 | 357 | /** 358 | * Get the charset when present or undefined. 359 | * 360 | * @return {String} 361 | * @api public 362 | */ 363 | 364 | get charset () { 365 | try { 366 | const { parameters } = contentType.parse(this.req) 367 | return parameters.charset || '' 368 | } catch (e) { 369 | return '' 370 | } 371 | }, 372 | 373 | /** 374 | * Return parsed Content-Length when present. 375 | * 376 | * @return {Number} 377 | * @api public 378 | */ 379 | 380 | get length () { 381 | const len = this.get('Content-Length') 382 | if (len === '') return 383 | return ~~len 384 | }, 385 | 386 | /** 387 | * Return the protocol string "http" or "https" 388 | * when requested with TLS. When the proxy setting 389 | * is enabled the "X-Forwarded-Proto" header 390 | * field will be trusted. If you're running behind 391 | * a reverse proxy that supplies https for you this 392 | * may be enabled. 393 | * 394 | * @return {String} 395 | * @api public 396 | */ 397 | 398 | get protocol () { 399 | if (this.socket.encrypted) return 'https' 400 | if (!this.app.proxy) return 'http' 401 | const proto = this.get('X-Forwarded-Proto') 402 | return proto ? proto.split(/\s*,\s*/, 1)[0] : 'http' 403 | }, 404 | 405 | /** 406 | * Short-hand for: 407 | * 408 | * this.protocol == 'https' 409 | * 410 | * @return {Boolean} 411 | * @api public 412 | */ 413 | 414 | get secure () { 415 | return this.protocol === 'https' 416 | }, 417 | 418 | /** 419 | * When `app.proxy` is `true`, parse 420 | * the "X-Forwarded-For" ip address list. 421 | * 422 | * For example if the value were "client, proxy1, proxy2" 423 | * you would receive the array `["client", "proxy1", "proxy2"]` 424 | * where "proxy2" is the furthest down-stream. 425 | * 426 | * @return {Array} 427 | * @api public 428 | */ 429 | 430 | get ips () { 431 | const proxy = this.app.proxy 432 | const val = this.get(this.app.proxyIpHeader) 433 | let ips = proxy && val 434 | ? val.split(/\s*,\s*/) 435 | : [] 436 | if (this.app.maxIpsCount > 0) { 437 | ips = ips.slice(-this.app.maxIpsCount) 438 | } 439 | return ips 440 | }, 441 | 442 | /** 443 | * Return request's remote address 444 | * When `app.proxy` is `true`, parse 445 | * the "X-Forwarded-For" ip address list and return the first one 446 | * 447 | * @return {String} 448 | * @api public 449 | */ 450 | 451 | get ip () { 452 | if (!this[IP]) { 453 | this[IP] = this.ips[0] || this.socket.remoteAddress || '' 454 | } 455 | return this[IP] 456 | }, 457 | 458 | set ip (_ip) { 459 | this[IP] = _ip 460 | }, 461 | 462 | /** 463 | * Return subdomains as an array. 464 | * 465 | * Subdomains are the dot-separated parts of the host before the main domain 466 | * of the app. By default, the domain of the app is assumed to be the last two 467 | * parts of the host. This can be changed by setting `app.subdomainOffset`. 468 | * 469 | * For example, if the domain is "tobi.ferrets.example.com": 470 | * If `app.subdomainOffset` is not set, this.subdomains is 471 | * `["ferrets", "tobi"]`. 472 | * If `app.subdomainOffset` is 3, this.subdomains is `["tobi"]`. 473 | * 474 | * @return {Array} 475 | * @api public 476 | */ 477 | 478 | get subdomains () { 479 | const offset = this.app.subdomainOffset 480 | const hostname = this.hostname 481 | if (net.isIP(hostname)) return [] 482 | return hostname 483 | .split('.') 484 | .reverse() 485 | .slice(offset) 486 | }, 487 | 488 | /** 489 | * Get accept object. 490 | * Lazily memoized. 491 | * 492 | * @return {Object} 493 | * @api private 494 | */ 495 | get accept () { 496 | return this._accept || (this._accept = accepts(this.req)) 497 | }, 498 | 499 | /** 500 | * Set accept object. 501 | * 502 | * @param {Object} 503 | * @api private 504 | */ 505 | set accept (obj) { 506 | this._accept = obj 507 | }, 508 | 509 | /** 510 | * Check if the given `type(s)` is acceptable, returning 511 | * the best match when true, otherwise `false`, in which 512 | * case you should respond with 406 "Not Acceptable". 513 | * 514 | * The `type` value may be a single mime type string 515 | * such as "application/json", the extension name 516 | * such as "json" or an array `["json", "html", "text/plain"]`. When a list 517 | * or array is given the _best_ match, if any is returned. 518 | * 519 | * Examples: 520 | * 521 | * // Accept: text/html 522 | * this.accepts('html'); 523 | * // => "html" 524 | * 525 | * // Accept: text/*, application/json 526 | * this.accepts('html'); 527 | * // => "html" 528 | * this.accepts('text/html'); 529 | * // => "text/html" 530 | * this.accepts('json', 'text'); 531 | * // => "json" 532 | * this.accepts('application/json'); 533 | * // => "application/json" 534 | * 535 | * // Accept: text/*, application/json 536 | * this.accepts('image/png'); 537 | * this.accepts('png'); 538 | * // => false 539 | * 540 | * // Accept: text/*;q=.5, application/json 541 | * this.accepts(['html', 'json']); 542 | * this.accepts('html', 'json'); 543 | * // => "json" 544 | * 545 | * @param {String|Array} type(s)... 546 | * @return {String|Array|false} 547 | * @api public 548 | */ 549 | 550 | accepts (...args) { 551 | return this.accept.types(...args) 552 | }, 553 | 554 | /** 555 | * Return accepted encodings or best fit based on `encodings`. 556 | * 557 | * Given `Accept-Encoding: gzip, deflate` 558 | * an array sorted by quality is returned: 559 | * 560 | * ['gzip', 'deflate'] 561 | * 562 | * @param {String|Array} encoding(s)... 563 | * @return {String|Array} 564 | * @api public 565 | */ 566 | 567 | acceptsEncodings (...args) { 568 | return this.accept.encodings(...args) 569 | }, 570 | 571 | /** 572 | * Return accepted charsets or best fit based on `charsets`. 573 | * 574 | * Given `Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5` 575 | * an array sorted by quality is returned: 576 | * 577 | * ['utf-8', 'utf-7', 'iso-8859-1'] 578 | * 579 | * @param {String|Array} charset(s)... 580 | * @return {String|Array} 581 | * @api public 582 | */ 583 | 584 | acceptsCharsets (...args) { 585 | return this.accept.charsets(...args) 586 | }, 587 | 588 | /** 589 | * Return accepted languages or best fit based on `langs`. 590 | * 591 | * Given `Accept-Language: en;q=0.8, es, pt` 592 | * an array sorted by quality is returned: 593 | * 594 | * ['es', 'pt', 'en'] 595 | * 596 | * @param {String|Array} lang(s)... 597 | * @return {Array|String} 598 | * @api public 599 | */ 600 | 601 | acceptsLanguages (...args) { 602 | return this.accept.languages(...args) 603 | }, 604 | 605 | /** 606 | * Check if the incoming request contains the "Content-Type" 607 | * header field, and it contains any of the give mime `type`s. 608 | * If there is no request body, `null` is returned. 609 | * If there is no content type, `false` is returned. 610 | * Otherwise, it returns the first `type` that matches. 611 | * 612 | * Examples: 613 | * 614 | * // With Content-Type: text/html; charset=utf-8 615 | * this.is('html'); // => 'html' 616 | * this.is('text/html'); // => 'text/html' 617 | * this.is('text/*', 'application/json'); // => 'text/html' 618 | * 619 | * // When Content-Type is application/json 620 | * this.is('json', 'urlencoded'); // => 'json' 621 | * this.is('application/json'); // => 'application/json' 622 | * this.is('html', 'application/*'); // => 'application/json' 623 | * 624 | * this.is('html'); // => false 625 | * 626 | * @param {String|String[]} [type] 627 | * @param {String[]} [types] 628 | * @return {String|false|null} 629 | * @api public 630 | */ 631 | 632 | is (type, ...types) { 633 | return typeis(this.req, type, ...types) 634 | }, 635 | 636 | /** 637 | * Return the request mime type void of 638 | * parameters such as "charset". 639 | * 640 | * @return {String} 641 | * @api public 642 | */ 643 | 644 | get type () { 645 | const type = this.get('Content-Type') 646 | if (!type) return '' 647 | return type.split(';')[0] 648 | }, 649 | 650 | /** 651 | * Return request header. 652 | * 653 | * The `Referrer` header field is special-cased, 654 | * both `Referrer` and `Referer` are interchangeable. 655 | * 656 | * Examples: 657 | * 658 | * this.get('Content-Type'); 659 | * // => "text/plain" 660 | * 661 | * this.get('content-type'); 662 | * // => "text/plain" 663 | * 664 | * this.get('Something'); 665 | * // => '' 666 | * 667 | * @param {String} field 668 | * @return {String} 669 | * @api public 670 | */ 671 | 672 | get (field) { 673 | const req = this.req 674 | switch (field = field.toLowerCase()) { 675 | case 'referer': 676 | case 'referrer': 677 | return req.headers.referrer || req.headers.referer || '' 678 | default: 679 | return req.headers[field] || '' 680 | } 681 | }, 682 | 683 | /** 684 | * Inspect implementation. 685 | * 686 | * @return {Object} 687 | * @api public 688 | */ 689 | 690 | inspect () { 691 | if (!this.req) return 692 | return this.toJSON() 693 | }, 694 | 695 | /** 696 | * Return JSON representation. 697 | * 698 | * @return {Object} 699 | * @api public 700 | */ 701 | 702 | toJSON () { 703 | return only(this, [ 704 | 'method', 705 | 'url', 706 | 'header' 707 | ]) 708 | } 709 | } 710 | 711 | /** 712 | * Custom inspection implementation for newer Node.js versions. 713 | * 714 | * @return {Object} 715 | * @api public 716 | */ 717 | 718 | /* istanbul ignore else */ 719 | if (util.inspect.custom) { 720 | module.exports[util.inspect.custom] = module.exports.inspect 721 | } 722 | --------------------------------------------------------------------------------