├── .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 |
--------------------------------------------------------------------------------