sumor 1.3.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +6 -0
- package/.eslintrc.yml +4 -0
- package/.gitattributes +1 -0
- package/.github/workflows/audit.yml +22 -0
- package/.github/workflows/available.yml +22 -0
- package/.github/workflows/ci.yml +45 -0
- package/.github/workflows/coverage.yml +23 -0
- package/.github/workflows/publish.yml +31 -0
- package/.github/workflows/ut.yml +24 -0
- package/.husky/pre-commit +10 -0
- package/.husky/pre-push +10 -0
- package/.prettierrc.yml +7 -0
- package/LICENSE +21 -0
- package/demo/api/error.js +3 -0
- package/demo/api/file.js +4 -0
- package/demo/api/file.yaml +11 -0
- package/demo/api/hello.js +4 -0
- package/demo/api/hello.yaml +13 -0
- package/demo/api/login.js +4 -0
- package/demo/api/logout.js +4 -0
- package/demo/api/long.js +9 -0
- package/demo/api/long.yaml +2 -0
- package/demo/api/user/admin.js +12 -0
- package/demo/api/user/info.js +8 -0
- package/demo/config/config.yaml +2 -0
- package/demo/i18n/demo.yaml +1 -0
- package/demo/package.json +6 -0
- package/i18n/README.md +32 -0
- package/i18n/sumor_api.yaml +16 -0
- package/i18n/sumor_app.yaml +3 -0
- package/i18n/sumor_internal.yaml +1 -0
- package/i18n/sumor_token.yaml +2 -0
- package/i18nExt/sumor_api.de.yaml +3 -0
- package/i18nExt/sumor_api.en.yaml +3 -0
- package/i18nExt/sumor_api.es.yaml +3 -0
- package/i18nExt/sumor_api.fr.yaml +3 -0
- package/i18nExt/sumor_api.it.yaml +3 -0
- package/i18nExt/sumor_api.ja.yaml +3 -0
- package/i18nExt/sumor_api.ko.yaml +3 -0
- package/i18nExt/sumor_api.pt.yaml +3 -0
- package/i18nExt/sumor_api.zh-TW.yaml +3 -0
- package/i18nExt/sumor_api.zh.yaml +3 -0
- package/i18nExt/sumor_app.de.yaml +3 -0
- package/i18nExt/sumor_app.en.yaml +3 -0
- package/i18nExt/sumor_app.es.yaml +3 -0
- package/i18nExt/sumor_app.zh-TW.yaml +3 -0
- package/i18nExt/sumor_app.zh.yaml +3 -0
- package/i18nExt/sumor_internal.de.yaml +1 -0
- package/i18nExt/sumor_internal.en.yaml +1 -0
- package/i18nExt/sumor_internal.es.yaml +1 -0
- package/i18nExt/sumor_internal.zh-TW.yaml +1 -0
- package/i18nExt/sumor_internal.zh.yaml +1 -0
- package/i18nExt/sumor_token.de.yaml +2 -0
- package/i18nExt/sumor_token.es.yaml +2 -0
- package/i18nExt/sumor_token.fr.yaml +2 -0
- package/i18nExt/sumor_token.it.yaml +2 -0
- package/i18nExt/sumor_token.ja.yaml +2 -0
- package/i18nExt/sumor_token.ko.yaml +2 -0
- package/i18nExt/sumor_token.pt.yaml +2 -0
- package/i18nExt/sumor_token.ru.yaml +2 -0
- package/i18nExt/sumor_token.tr.yaml +2 -0
- package/i18nExt/sumor_token.zh-TW.yaml +2 -0
- package/i18nExt/sumor_token.zh.yaml +2 -0
- package/jest.config.json +26 -0
- package/modules/alertPage/html/template.html +162 -0
- package/modules/alertPage/html/undraw_access-denied.svg +52 -0
- package/modules/alertPage/html/undraw_alert.svg +1 -0
- package/modules/alertPage/html/undraw_celebration.svg +78 -0
- package/modules/alertPage/html/undraw_complete-form.svg +1 -0
- package/modules/alertPage/html/undraw_login.svg +1 -0
- package/modules/alertPage/index.js +46 -0
- package/modules/alertPage/index.test.js +12 -0
- package/modules/i18n/README.md +90 -0
- package/modules/i18n/convertI18nValue/README.md +31 -0
- package/modules/i18n/convertI18nValue/getI18nTemplate.js +26 -0
- package/modules/i18n/convertI18nValue/getI18nTemplate.test.js +40 -0
- package/modules/i18n/convertI18nValue/index.js +14 -0
- package/modules/i18n/convertI18nValue/index.test.js +55 -0
- package/modules/i18n/convertI18nValue/stringVariableReplace.js +13 -0
- package/modules/i18n/convertI18nValue/stringVariableReplace.test.js +39 -0
- package/modules/i18n/index.js +26 -0
- package/modules/i18n/index.test.js +13 -0
- package/modules/i18n/load/README.md +28 -0
- package/modules/i18n/load/load.js +31 -0
- package/modules/i18n/load/load.test.js +30 -0
- package/modules/i18n/registry.js +48 -0
- package/modules/i18n/registry.test.js +84 -0
- package/modules/logger/convert/parseFile.js +14 -0
- package/modules/logger/convert/parseFile.test.js +28 -0
- package/modules/logger/convert/stringifyCMD.js +48 -0
- package/modules/logger/convert/stringifyCMD.test.js +37 -0
- package/modules/logger/convert/stringifyFile.js +24 -0
- package/modules/logger/convert/stringifyFile.test.js +79 -0
- package/modules/logger/index.js +82 -0
- package/modules/logger/index.test.js +124 -0
- package/modules/logger/logFileOperator.js +50 -0
- package/modules/logger/logFileOperator.test.js +69 -0
- package/modules/middlewares/apiMiddleware/errorCatcher.js +9 -0
- package/modules/middlewares/apiMiddleware/exposeApis/index.js +82 -0
- package/modules/middlewares/apiMiddleware/index.js +111 -0
- package/modules/middlewares/apiMiddleware/index.test.js +145 -0
- package/modules/middlewares/apiMiddleware/load/index.js +35 -0
- package/modules/middlewares/apiMiddleware/load/index.test.js +30 -0
- package/modules/middlewares/apiMiddleware/metadataToSwagger.js +139 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/caseSensitive.js +26 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/caseSensitive.test.js +36 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/convertType.js +48 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/convertType.test.js +53 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/defaultValue.js +31 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/defaultValue.test.js +31 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/index.js +18 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/index.test.js +40 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/precision.js +12 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/precision.test.js +33 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/trim.js +15 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/trim.test.js +24 -0
- package/modules/middlewares/apiMiddleware/prepareData/index.js +29 -0
- package/modules/middlewares/apiMiddleware/prepareData/index.test.js +121 -0
- package/modules/middlewares/apiMiddleware/prepareData/validate/checkLength.js +26 -0
- package/modules/middlewares/apiMiddleware/prepareData/validate/checkLength.test.js +52 -0
- package/modules/middlewares/apiMiddleware/prepareData/validate/index.js +30 -0
- package/modules/middlewares/apiMiddleware/public/favicon.ico +0 -0
- package/modules/middlewares/apiMiddleware/response/sendError.js +57 -0
- package/modules/middlewares/apiMiddleware/response/sendError.test.js +251 -0
- package/modules/middlewares/apiMiddleware/response/sendNotFound.js +26 -0
- package/modules/middlewares/apiMiddleware/response/sendResponse.js +25 -0
- package/modules/middlewares/apiMiddleware/response/sendSuccess.js +30 -0
- package/modules/middlewares/bodyMiddleware/cleanupFiles.js +14 -0
- package/modules/middlewares/bodyMiddleware/cleanupFiles.test.js +54 -0
- package/modules/middlewares/bodyMiddleware/fileParser.js +69 -0
- package/modules/middlewares/bodyMiddleware/fileParser.test.js +163 -0
- package/modules/middlewares/bodyMiddleware/index.js +12 -0
- package/modules/middlewares/bodyMiddleware/index.test.js +64 -0
- package/modules/middlewares/bodyMiddleware/mergeData.js +4 -0
- package/modules/middlewares/bodyMiddleware/mergeData.test.js +38 -0
- package/modules/middlewares/i18nMiddleware/index.js +82 -0
- package/modules/middlewares/i18nMiddleware/index.test.js +75 -0
- package/modules/middlewares/tokenMiddleware/Token.js +115 -0
- package/modules/middlewares/tokenMiddleware/Token.test.js +67 -0
- package/modules/middlewares/tokenMiddleware/index.js +32 -0
- package/modules/middlewares/tokenMiddleware/index.test.js +115 -0
- package/modules/middlewares/tokenMiddleware/parseCookie.js +24 -0
- package/modules/middlewares/tokenMiddleware/parseCookie.test.js +49 -0
- package/modules/serve/formatConfig.js +7 -0
- package/modules/serve/formatConfig.test.js +28 -0
- package/modules/serve/index.js +30 -0
- package/modules/serve/index.test.js +69 -0
- package/modules/serve/listenApp.js +11 -0
- package/modules/system/getSystemLanguage.js +9 -0
- package/modules/system/getSystemLanguage.test.js +19 -0
- package/modules/utils/getError.js +56 -0
- package/modules/utils/getError.test.js +97 -0
- package/modules/utils/loadConfig.js +73 -0
- package/modules/utils/loadConfig.test.js +129 -0
- package/modules/utils/pathUtils.js +43 -0
- package/modules/utils/pathUtils.test.js +52 -0
- package/modules/utils/type.js +14 -0
- package/modules/utils/type.test.js +40 -0
- package/package.json +61 -1
- package/src/app.js +40 -0
- package/src/cli.js +28 -0
- package/src/index.js +3 -0
- package/test-utils/codeTest/README.md +4 -0
- package/test-utils/codeTest/custom.css +47 -0
- package/test-utils/codeTest/index.js +80 -0
- package/test-utils/codeTest/utils/calculateCoverage.js +55 -0
- package/test-utils/codeTest/utils/getTestFiles.js +20 -0
- package/test-utils/codeTest/utils/runTests.js +38 -0
- package/test-utils/codeTest/utils/startServer.js +23 -0
- package/test-utils/tmp.js +15 -0
- package/cli.js +0 -3
- package/index.es.js +0 -578
- package/template/web/AppWithFrame.vue +0 -20
- package/template/web/index.html +0 -19
- package/template/web/src/App.vue +0 -15
- package/template/web/src/entry-client.js +0 -9
- package/template/web/src/entry-server.js +0 -69
- package/template/web/src/main.js +0 -41
- package/template/web/src/style.scss +0 -389
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import Token from './Token.js'
|
|
2
|
+
import parseCookie from './parseCookie.js'
|
|
3
|
+
|
|
4
|
+
export default app => {
|
|
5
|
+
if (app.namespace) {
|
|
6
|
+
app.use((req, res, next) => {
|
|
7
|
+
const authorization = req.headers.authorization
|
|
8
|
+
if (authorization) {
|
|
9
|
+
req.token = new Token(req)
|
|
10
|
+
if (authorization.startsWith('Bearer ')) {
|
|
11
|
+
req.token._id = authorization.substring(7)
|
|
12
|
+
}
|
|
13
|
+
} else {
|
|
14
|
+
req.token = new Token(req, id => {
|
|
15
|
+
let existingCookie = res.getHeader('Set-Cookie') || []
|
|
16
|
+
if (typeof existingCookie === 'string') {
|
|
17
|
+
existingCookie = [existingCookie]
|
|
18
|
+
}
|
|
19
|
+
const maxAge = 100 * 24 * 60 * 60
|
|
20
|
+
const newCookie = `t=${id}; Path=/; HttpOnly; Max-Age=${maxAge}`
|
|
21
|
+
existingCookie = existingCookie.filter(c => !c.startsWith('t='))
|
|
22
|
+
res.setHeader('Set-Cookie', [...existingCookie, newCookie])
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const cookie = parseCookie(req.headers.cookie)
|
|
26
|
+
req.token._id = cookie.t || null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
next()
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, it, beforeAll, afterAll, expect } from '@jest/globals'
|
|
2
|
+
import axios from 'axios'
|
|
3
|
+
import express from 'express'
|
|
4
|
+
import tokenMiddleware from './index.js'
|
|
5
|
+
import i18nMiddleware from '../i18nMiddleware/index.js'
|
|
6
|
+
import http from 'http'
|
|
7
|
+
|
|
8
|
+
describe('Token Middleware 测试', () => {
|
|
9
|
+
let app
|
|
10
|
+
let server
|
|
11
|
+
let baseURL
|
|
12
|
+
|
|
13
|
+
beforeAll(async () => {
|
|
14
|
+
app = express()
|
|
15
|
+
await i18nMiddleware(app)
|
|
16
|
+
tokenMiddleware(app)
|
|
17
|
+
|
|
18
|
+
app.get('/test', (req, res) => {
|
|
19
|
+
res.json({ token: req.token.id })
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
app.get('/login', (req, res) => {
|
|
23
|
+
req.token.id = 'test-token'
|
|
24
|
+
res.json({ token: req.token.id })
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
server = http.createServer(app)
|
|
28
|
+
await new Promise(resolve => {
|
|
29
|
+
server.listen(() => {
|
|
30
|
+
const { port } = server.address()
|
|
31
|
+
baseURL = `http://localhost:${port}`
|
|
32
|
+
resolve()
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
afterAll(done => {
|
|
38
|
+
server.close(done)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('应从 Authorization 头中提取 Bearer token', async () => {
|
|
42
|
+
const response = await axios.get(`${baseURL}/test`, {
|
|
43
|
+
headers: {
|
|
44
|
+
Authorization: 'Bearer test-token'
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
expect(response.data.token).toBe('test-token')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('如存在 Authorization 头,但不以 Bearer 开头,则不读取token', async () => {
|
|
52
|
+
const response = await axios.get(`${baseURL}/test`, {
|
|
53
|
+
headers: {
|
|
54
|
+
Authorization: 'test-token'
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
expect(response.data.token).toBe('')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('应从 Cookie 中提取 token', async () => {
|
|
62
|
+
const response = await axios.get(`${baseURL}/test`, {
|
|
63
|
+
headers: {
|
|
64
|
+
Cookie: 't=test-cookie-token'
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
expect(response.data.token).toBe('test-cookie-token')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('应设置新的 token 到 Cookie 中', async () => {
|
|
72
|
+
const response = await axios.get(`${baseURL}/login`)
|
|
73
|
+
|
|
74
|
+
const setCookieHeader = response.headers['set-cookie']
|
|
75
|
+
expect(setCookieHeader).toBeDefined()
|
|
76
|
+
expect(setCookieHeader[0]).toMatch(/t=.+; Path=\/; HttpOnly; Max-Age=8640000/)
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('Token Middleware 测试,无namespace中间件', () => {
|
|
81
|
+
let app
|
|
82
|
+
let server
|
|
83
|
+
let baseURL
|
|
84
|
+
|
|
85
|
+
beforeAll(async () => {
|
|
86
|
+
app = express()
|
|
87
|
+
tokenMiddleware(app)
|
|
88
|
+
|
|
89
|
+
app.get('/test', (req, res) => {
|
|
90
|
+
if (!req.token) {
|
|
91
|
+
res.send('NO_TOKEN')
|
|
92
|
+
} else {
|
|
93
|
+
res.send('HAS_TOKEN')
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
server = http.createServer(app)
|
|
98
|
+
await new Promise(resolve => {
|
|
99
|
+
server.listen(() => {
|
|
100
|
+
const { port } = server.address()
|
|
101
|
+
baseURL = `http://localhost:${port}`
|
|
102
|
+
resolve()
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
afterAll(done => {
|
|
108
|
+
server.close(done)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('检测TOKEN,不应存在', async () => {
|
|
112
|
+
const response = await axios.get(`${baseURL}/test`)
|
|
113
|
+
expect(response.data).toBe('NO_TOKEN')
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// 扩展 parseCookie 函数,增加详细的变量命名和中文备注
|
|
2
|
+
export default function parseCookie(cookieString) {
|
|
3
|
+
// 如果 cookieString 是 null 或 undefined,则将其设置为空字符串
|
|
4
|
+
cookieString = cookieString || ''
|
|
5
|
+
|
|
6
|
+
// 将 cookieString 按照分号分割成数组
|
|
7
|
+
let cookieArray = cookieString.split(';')
|
|
8
|
+
|
|
9
|
+
// 如果分割后的数组第一个元素是空字符串,则将数组置为空数组
|
|
10
|
+
if (cookieArray[0] === '') {
|
|
11
|
+
cookieArray = []
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// 使用 reduce 方法将 cookieArray 转换为键值对对象
|
|
15
|
+
return cookieArray.reduce((cookieObject, cookieItem) => {
|
|
16
|
+
// 将每个 cookie 项按照等号分割为键和值
|
|
17
|
+
const [key, value] = cookieItem.split('=')
|
|
18
|
+
|
|
19
|
+
// 去除键的多余空格,并将键值对存入结果对象
|
|
20
|
+
cookieObject[key.trim()] = value.trim() || null // 如果值不存在,则设置为 null
|
|
21
|
+
|
|
22
|
+
return cookieObject // 返回累积的结果对象
|
|
23
|
+
}, {}) // 初始值为一个空对象
|
|
24
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, test, expect } from '@jest/globals'
|
|
2
|
+
import parseCookie from './parseCookie'
|
|
3
|
+
|
|
4
|
+
// 测试文件主要用于验证 parseCookie 函数的功能是否正确
|
|
5
|
+
|
|
6
|
+
describe('parseCookie 函数测试', () => {
|
|
7
|
+
// 测试:正常解析 cookie 字符串
|
|
8
|
+
test('应该正确解析有效的 cookie 字符串', () => {
|
|
9
|
+
const cookieString = 'key1=value1; key2=value2'
|
|
10
|
+
const expectedOutput = {
|
|
11
|
+
key1: 'value1',
|
|
12
|
+
key2: 'value2'
|
|
13
|
+
}
|
|
14
|
+
expect(parseCookie(cookieString)).toEqual(expectedOutput)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
// 测试:处理空字符串
|
|
18
|
+
test('应该返回空对象当输入为空字符串时', () => {
|
|
19
|
+
const cookieString = ''
|
|
20
|
+
const expectedOutput = {}
|
|
21
|
+
expect(parseCookie(cookieString)).toEqual(expectedOutput)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// 测试:处理 null 或 undefined
|
|
25
|
+
test('应该返回空对象当输入为 null 或 undefined 时', () => {
|
|
26
|
+
expect(parseCookie(null)).toEqual({})
|
|
27
|
+
expect(parseCookie(undefined)).toEqual({})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// 测试:处理没有值的键
|
|
31
|
+
test('应该将没有值的键的值设置为 null', () => {
|
|
32
|
+
const cookieString = 'key1=; key2=value2'
|
|
33
|
+
const expectedOutput = {
|
|
34
|
+
key1: null,
|
|
35
|
+
key2: 'value2'
|
|
36
|
+
}
|
|
37
|
+
expect(parseCookie(cookieString)).toEqual(expectedOutput)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// 测试:处理多余的空格
|
|
41
|
+
test('应该正确处理键和值周围的多余空格', () => {
|
|
42
|
+
const cookieString = ' key1 = value1 ; key2 = value2 '
|
|
43
|
+
const expectedOutput = {
|
|
44
|
+
key1: 'value1',
|
|
45
|
+
key2: 'value2'
|
|
46
|
+
}
|
|
47
|
+
expect(parseCookie(cookieString)).toEqual(expectedOutput)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals'
|
|
2
|
+
import format from './formatConfig.js'
|
|
3
|
+
|
|
4
|
+
describe('format function', () => {
|
|
5
|
+
it('当未提供配置时应返回默认值', () => {
|
|
6
|
+
const result = format()
|
|
7
|
+
expect(result.port).toBe(80)
|
|
8
|
+
expect(result.name).toBe('轻呈云应用')
|
|
9
|
+
expect(result.desc).toBe('')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('当提供带端口的配置时应覆盖端口', () => {
|
|
13
|
+
const result = format({ port: 3000 })
|
|
14
|
+
expect(result.port).toBe(3000)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('应保留配置对象中的其他属性', () => {
|
|
18
|
+
const result = format({ port: 3000, host: 'localhost' })
|
|
19
|
+
expect(result.port).toBe(3000)
|
|
20
|
+
expect(result.host).toBe('localhost')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('当提供name和desc时应覆盖默认值', () => {
|
|
24
|
+
const result = format({ name: '测试应用', desc: '这是一个测试应用' })
|
|
25
|
+
expect(result.name).toBe('测试应用')
|
|
26
|
+
expect(result.desc).toBe('这是一个测试应用')
|
|
27
|
+
})
|
|
28
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import listenApp from './listenApp.js'
|
|
3
|
+
import format from './formatConfig.js'
|
|
4
|
+
|
|
5
|
+
export default async function (config, callbacks) {
|
|
6
|
+
config = format(config)
|
|
7
|
+
const app = express()
|
|
8
|
+
|
|
9
|
+
// 添加交互参数
|
|
10
|
+
app.sumor = {
|
|
11
|
+
config
|
|
12
|
+
}
|
|
13
|
+
app.use((req, res, next) => {
|
|
14
|
+
req.sumor = {
|
|
15
|
+
config
|
|
16
|
+
}
|
|
17
|
+
next()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
if (typeof callbacks === 'object') {
|
|
21
|
+
for (const callback of callbacks) {
|
|
22
|
+
await callback(app)
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
await callbacks(app)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const close = await listenApp(app, config.port)
|
|
29
|
+
return { app, config, close }
|
|
30
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, it, expect, jest } from '@jest/globals'
|
|
2
|
+
import serve from './index.js'
|
|
3
|
+
|
|
4
|
+
describe('serve', () => {
|
|
5
|
+
it('应该执行单个回调函数', async () => {
|
|
6
|
+
const mockCallback = jest.fn()
|
|
7
|
+
const config = {
|
|
8
|
+
port: Math.floor(Math.random() * (65535 - 1024) + 1024)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { app, close } = await serve(config, mockCallback)
|
|
12
|
+
|
|
13
|
+
expect(mockCallback).toHaveBeenCalledTimes(1)
|
|
14
|
+
expect(mockCallback).toHaveBeenCalledWith(app)
|
|
15
|
+
|
|
16
|
+
await close()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('应该按顺序执行多个回调函数', async () => {
|
|
20
|
+
const mockCallback1 = jest.fn()
|
|
21
|
+
const mockCallback2 = jest.fn()
|
|
22
|
+
const config = {
|
|
23
|
+
port: Math.floor(Math.random() * (65535 - 1024) + 1024)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { app, close } = await serve(config, [mockCallback1, mockCallback2])
|
|
27
|
+
|
|
28
|
+
expect(mockCallback1).toHaveBeenCalledTimes(1)
|
|
29
|
+
expect(mockCallback1).toHaveBeenCalledWith(app)
|
|
30
|
+
expect(mockCallback2).toHaveBeenCalledTimes(1)
|
|
31
|
+
expect(mockCallback2).toHaveBeenCalledWith(app)
|
|
32
|
+
|
|
33
|
+
await close()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('应该启动并停止服务器', async () => {
|
|
37
|
+
const mockCallback = jest.fn()
|
|
38
|
+
const config = {
|
|
39
|
+
port: Math.floor(Math.random() * (65535 - 1024) + 1024)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { close } = await serve(config, mockCallback)
|
|
43
|
+
|
|
44
|
+
expect(typeof close).toBe('function')
|
|
45
|
+
|
|
46
|
+
await close()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('应该在请求中添加 sumor 对象', async () => {
|
|
50
|
+
const mockCallback = app => {
|
|
51
|
+
expect(app.sumor).toBeDefined()
|
|
52
|
+
app.get('/', (req, res) => {
|
|
53
|
+
if (req.sumor) {
|
|
54
|
+
res.send('OK')
|
|
55
|
+
} else {
|
|
56
|
+
res.send('FAIL')
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
const config = {
|
|
61
|
+
port: Math.floor(Math.random() * (65535 - 1024) + 1024)
|
|
62
|
+
}
|
|
63
|
+
const { close } = await serve(config, mockCallback)
|
|
64
|
+
const response = await fetch(`http://localhost:${config.port}/`)
|
|
65
|
+
const text = await response.text()
|
|
66
|
+
expect(text).toBe('OK')
|
|
67
|
+
await close()
|
|
68
|
+
})
|
|
69
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals'
|
|
2
|
+
import getSystemLanguage from './getSystemLanguage.js'
|
|
3
|
+
import { osLocale } from 'os-locale'
|
|
4
|
+
describe('i18n Middleware', () => {
|
|
5
|
+
it('加载系统语言', async () => {
|
|
6
|
+
delete process.env.LANGUAGE
|
|
7
|
+
const language = await getSystemLanguage()
|
|
8
|
+
const expectLanguage = await osLocale()
|
|
9
|
+
expect(language).toBe(expectLanguage)
|
|
10
|
+
|
|
11
|
+
process.env.LANGUAGE = 'zh-CN'
|
|
12
|
+
const language2 = await getSystemLanguage()
|
|
13
|
+
expect(language2).toBe('zh-CN')
|
|
14
|
+
|
|
15
|
+
process.env.LANGUAGE = 'en-US'
|
|
16
|
+
const language3 = await getSystemLanguage()
|
|
17
|
+
expect(language3).toBe('en-US')
|
|
18
|
+
})
|
|
19
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type from './type.js'
|
|
2
|
+
|
|
3
|
+
export default ({ namespace, transform = () => {} }) => {
|
|
4
|
+
const lowerCaseNamespace = namespace.toLowerCase()
|
|
5
|
+
const capitalizedNamespace =
|
|
6
|
+
lowerCaseNamespace.charAt(0).toUpperCase() + lowerCaseNamespace.slice(1)
|
|
7
|
+
|
|
8
|
+
return class SumorError extends Error {
|
|
9
|
+
constructor(code, data, errors) {
|
|
10
|
+
super()
|
|
11
|
+
this.name = `${capitalizedNamespace}Error`
|
|
12
|
+
this.namespace = lowerCaseNamespace
|
|
13
|
+
this.code = code
|
|
14
|
+
|
|
15
|
+
this.errors = []
|
|
16
|
+
if (errors) {
|
|
17
|
+
if (errors instanceof Error) {
|
|
18
|
+
this.errors = [errors]
|
|
19
|
+
} else if (Array.isArray(errors)) {
|
|
20
|
+
this.errors = errors.filter(error => error instanceof Error)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.data = type(data) === 'object' ? data : {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
set message(_) {
|
|
28
|
+
throw new Error('message is readonly, please use code and data to set message.')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get message() {
|
|
32
|
+
return transform(namespace, this.code, this.data) || this.code
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
json() {
|
|
36
|
+
const errors = this.errors.map(error => {
|
|
37
|
+
if (error.json) {
|
|
38
|
+
return error.json()
|
|
39
|
+
} else {
|
|
40
|
+
return {
|
|
41
|
+
code: error.code || 'UNKNOWN_ERROR',
|
|
42
|
+
message: error.message,
|
|
43
|
+
data: error.data || {},
|
|
44
|
+
errors: []
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
return {
|
|
49
|
+
code: this.code,
|
|
50
|
+
message: this.message,
|
|
51
|
+
data: this.data,
|
|
52
|
+
errors
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals'
|
|
2
|
+
import getError from './getError'
|
|
3
|
+
|
|
4
|
+
describe('getError 函数', () => {
|
|
5
|
+
const transform = (namespace, code, data) => `${code}: ${JSON.stringify(data)}`
|
|
6
|
+
const options = { namespace: 'Test', transform }
|
|
7
|
+
const SumorError = getError(options)
|
|
8
|
+
|
|
9
|
+
it('无transform函数时,message应该等于code', () => {
|
|
10
|
+
const NonTransformError = getError({ namespace: 'NonTransform' })
|
|
11
|
+
const error = new NonTransformError('ERROR_CODE')
|
|
12
|
+
expect(error.code).toBe('ERROR_CODE')
|
|
13
|
+
expect(error.message).toBe('ERROR_CODE')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('应该创建一个具有正确名称和代码的错误对象', () => {
|
|
17
|
+
const error = new SumorError('ERROR_CODE', { key: 'value' })
|
|
18
|
+
expect(error.name).toBe('TestError')
|
|
19
|
+
expect(error.code).toBe('ERROR_CODE')
|
|
20
|
+
expect(error.data).toEqual({ key: 'value' })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('应该正确处理 errors 数组', () => {
|
|
24
|
+
const innerError = new Error('Inner error')
|
|
25
|
+
const error = new SumorError('ERROR_CODE', {}, [innerError])
|
|
26
|
+
expect(error.errors).toHaveLength(1)
|
|
27
|
+
expect(error.errors[0].message).toBe('Inner error')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('尝试直接设置 message 时应该抛出错误', () => {
|
|
31
|
+
const error = new SumorError('ERROR_CODE', {})
|
|
32
|
+
expect(() => {
|
|
33
|
+
error.message = 'New message'
|
|
34
|
+
}).toThrow('message is readonly, please use code and data to set message.')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('应该将单个 Error 对象正确包装到 errors 数组中', () => {
|
|
38
|
+
const innerError = new Error('Inner error')
|
|
39
|
+
const error = new SumorError('ERROR_CODE', {}, innerError)
|
|
40
|
+
expect(error.errors).toHaveLength(1)
|
|
41
|
+
expect(error.errors[0]).toBe(innerError)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('应该在 data 不是对象时将 data 设置为空对象', () => {
|
|
45
|
+
const error = new SumorError('ERROR_CODE', 'notAnObject')
|
|
46
|
+
expect(error.data).toEqual({})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('应该生成正确的 JSON 表示', () => {
|
|
50
|
+
const error = new SumorError('ERROR_CODE', { key: 'value' })
|
|
51
|
+
const json = error.json()
|
|
52
|
+
expect(json).toEqual({
|
|
53
|
+
code: 'ERROR_CODE',
|
|
54
|
+
message: 'ERROR_CODE: {"key":"value"}',
|
|
55
|
+
data: { key: 'value' },
|
|
56
|
+
errors: []
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('应该在 JSON 表示中包含嵌套的错误', () => {
|
|
61
|
+
const error1 = new SumorError('ERROR_CODE', { key: 'value1' })
|
|
62
|
+
const error2 = new SumorError('ERROR_CODE', { key: 'value2' }, error1)
|
|
63
|
+
const json = error2.json()
|
|
64
|
+
expect(json).toEqual({
|
|
65
|
+
code: 'ERROR_CODE',
|
|
66
|
+
message: 'ERROR_CODE: {"key":"value2"}',
|
|
67
|
+
data: { key: 'value2' },
|
|
68
|
+
errors: [
|
|
69
|
+
{
|
|
70
|
+
code: 'ERROR_CODE',
|
|
71
|
+
message: 'ERROR_CODE: {"key":"value1"}',
|
|
72
|
+
data: { key: 'value1' },
|
|
73
|
+
errors: []
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('应该在 JSON 表示中包含嵌套的其他错误', () => {
|
|
80
|
+
const error1 = new Error('Inner error 1')
|
|
81
|
+
const error2 = new SumorError('ERROR_CODE', { key: 'value2' }, error1)
|
|
82
|
+
const json = error2.json()
|
|
83
|
+
expect(json).toEqual({
|
|
84
|
+
code: 'ERROR_CODE',
|
|
85
|
+
message: 'ERROR_CODE: {"key":"value2"}',
|
|
86
|
+
data: { key: 'value2' },
|
|
87
|
+
errors: [
|
|
88
|
+
{
|
|
89
|
+
code: 'UNKNOWN_ERROR',
|
|
90
|
+
message: 'Inner error 1',
|
|
91
|
+
data: {},
|
|
92
|
+
errors: []
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import yaml from 'yaml'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { glob } from 'glob'
|
|
5
|
+
|
|
6
|
+
export default async (condition, root) => {
|
|
7
|
+
const extensions = ['.yaml', '.json', '.yml']
|
|
8
|
+
|
|
9
|
+
if (root) {
|
|
10
|
+
// 多个配置查询,返回列表对象,key为相对路径,值为配置对象
|
|
11
|
+
|
|
12
|
+
// 如果condition不包括后缀,则添加后缀,限制其仅允许yaml和json
|
|
13
|
+
if (!condition.includes('yaml') && !condition.includes('json') && !condition.includes('yml')) {
|
|
14
|
+
condition = `${condition}.{yaml,json,yml}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const files = await glob(condition, {
|
|
18
|
+
cwd: root
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
// 排序以确保后缀名优先级
|
|
22
|
+
files.sort((a, b) => {
|
|
23
|
+
const extA = path.extname(a)
|
|
24
|
+
const extB = path.extname(b)
|
|
25
|
+
return extensions.indexOf(extB) - extensions.indexOf(extA)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const result = {}
|
|
29
|
+
for (const file of files) {
|
|
30
|
+
const ext = path.extname(file)
|
|
31
|
+
const pathWithoutExt = file.replace(ext, '')
|
|
32
|
+
if (extensions.includes(ext)) {
|
|
33
|
+
const filePath = path.join(root, file)
|
|
34
|
+
const fileContent = fs.readFileSync(filePath, 'utf8')
|
|
35
|
+
if (ext === '.json') {
|
|
36
|
+
result[pathWithoutExt] = JSON.parse(fileContent)
|
|
37
|
+
} else {
|
|
38
|
+
result[pathWithoutExt] = yaml.parse(fileContent)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result
|
|
43
|
+
} else {
|
|
44
|
+
// 单个配置查询,返回该配置对象
|
|
45
|
+
const filePath = condition
|
|
46
|
+
const ext = path.extname(filePath)
|
|
47
|
+
|
|
48
|
+
// 带后缀,则直接返回
|
|
49
|
+
if (extensions.includes(ext)) {
|
|
50
|
+
const fileContent = fs.readFileSync(filePath, 'utf8')
|
|
51
|
+
if (ext === '.json') {
|
|
52
|
+
return JSON.parse(fileContent)
|
|
53
|
+
} else {
|
|
54
|
+
return yaml.parse(fileContent)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 不带后缀,尝试添加后缀
|
|
59
|
+
for (const ext of extensions) {
|
|
60
|
+
const fullPath = `${filePath}${ext}`
|
|
61
|
+
if (fs.existsSync(fullPath)) {
|
|
62
|
+
const fileContent = fs.readFileSync(fullPath, 'utf8')
|
|
63
|
+
if (ext === '.json') {
|
|
64
|
+
return JSON.parse(fileContent)
|
|
65
|
+
} else {
|
|
66
|
+
return yaml.parse(fileContent)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {}
|
|
72
|
+
}
|
|
73
|
+
}
|