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,129 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals'
|
|
2
|
+
import loadConfig from './loadConfig.js'
|
|
3
|
+
import tmp from '../../test-utils/tmp.js'
|
|
4
|
+
import fse from 'fs-extra'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
|
|
7
|
+
describe('loadConfig', () => {
|
|
8
|
+
const prepare = async () => {
|
|
9
|
+
const caseDir = await tmp('config')
|
|
10
|
+
return path.join(caseDir, 'main')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
it('应该将 JSON 配置文件加载为对象', async () => {
|
|
14
|
+
const configPath = await prepare()
|
|
15
|
+
const configContent = { key: 'value' }
|
|
16
|
+
await fse.writeFile(`${configPath}.json`, JSON.stringify(configContent))
|
|
17
|
+
const config = await loadConfig(configPath)
|
|
18
|
+
expect(config).toEqual(configContent)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('应该将 YAML 配置文件加载为对象', async () => {
|
|
22
|
+
const configPath = await prepare()
|
|
23
|
+
const configContent = { key: 'value' }
|
|
24
|
+
await fse.writeFile(`${configPath}.yaml`, 'key: value')
|
|
25
|
+
const config = await loadConfig(configPath)
|
|
26
|
+
expect(config).toEqual(configContent)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('如果未找到配置文件,则应返回一个空对象', async () => {
|
|
30
|
+
const configPath = await prepare()
|
|
31
|
+
const config = await loadConfig(configPath)
|
|
32
|
+
expect(config).toEqual({})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('应该按优先级加载配置文件: yaml > json > yml', async () => {
|
|
36
|
+
const configPath = await prepare()
|
|
37
|
+
const configContentJson = { key: 'json' }
|
|
38
|
+
const configContentYaml = { key: 'yaml' }
|
|
39
|
+
const configContentYml = { key: 'yml' }
|
|
40
|
+
|
|
41
|
+
let config
|
|
42
|
+
await fse.writeFile(`${configPath}.yml`, 'key: yml')
|
|
43
|
+
config = await loadConfig(configPath)
|
|
44
|
+
expect(config).toEqual(configContentYml)
|
|
45
|
+
await fse.writeFile(`${configPath}.json`, JSON.stringify(configContentJson))
|
|
46
|
+
config = await loadConfig(configPath)
|
|
47
|
+
expect(config).toEqual(configContentJson)
|
|
48
|
+
await fse.writeFile(`${configPath}.yml`, 'key: yml')
|
|
49
|
+
config = await loadConfig(configPath)
|
|
50
|
+
expect(config).toEqual(configContentJson)
|
|
51
|
+
await fse.writeFile(`${configPath}.yaml`, 'key: yaml')
|
|
52
|
+
config = await loadConfig(configPath)
|
|
53
|
+
expect(config).toEqual(configContentYaml)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('应该支持带有多个点的文件名', async () => {
|
|
57
|
+
const configPath = await prepare()
|
|
58
|
+
const configContent = { key: 'value' }
|
|
59
|
+
await fse.writeFile(`${configPath}.zh-CN.json`, JSON.stringify(configContent))
|
|
60
|
+
let config = await loadConfig(`${configPath}.zh-CN`)
|
|
61
|
+
expect(config).toEqual(configContent)
|
|
62
|
+
await fse.writeFile(`${configPath}.zh-CN.yml`, 'key: value')
|
|
63
|
+
config = await loadConfig(`${configPath}.zh-CN`)
|
|
64
|
+
expect(config).toEqual(configContent)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('应该支持路径中包含后缀名的情况 (yml)', async () => {
|
|
68
|
+
const configPath = await prepare()
|
|
69
|
+
const configContent = { key: 'value' }
|
|
70
|
+
await fse.writeFile(`${configPath}.yml`, 'key: value')
|
|
71
|
+
const config = await loadConfig(`${configPath}.yml`)
|
|
72
|
+
expect(config).toEqual(configContent)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('应该支持路径中包含后缀名的情况 (json)', async () => {
|
|
76
|
+
const configPath = await prepare()
|
|
77
|
+
const configContent = { key: 'value' }
|
|
78
|
+
await fse.writeFile(`${configPath}.json`, JSON.stringify(configContent))
|
|
79
|
+
const config = await loadConfig(`${configPath}.json`)
|
|
80
|
+
expect(config).toEqual(configContent)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('应该支持多个配置查询,条件为 *.yaml', async () => {
|
|
84
|
+
const rootDir = await tmp('config')
|
|
85
|
+
const yamlContent1 = { key: 'value1' }
|
|
86
|
+
const yamlContent2 = { key: 'value2' }
|
|
87
|
+
|
|
88
|
+
await fse.writeFile(path.join(rootDir, 'config1.yaml'), 'key: value1')
|
|
89
|
+
await fse.writeFile(path.join(rootDir, 'config2.yaml'), 'key: value2')
|
|
90
|
+
|
|
91
|
+
const configs = await loadConfig('*.yaml', rootDir)
|
|
92
|
+
expect(configs).toEqual({
|
|
93
|
+
config1: yamlContent1,
|
|
94
|
+
config2: yamlContent2
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('应该支持多个配置查询,条件为 *', async () => {
|
|
99
|
+
const rootDir = await tmp('config')
|
|
100
|
+
const yamlContent = { key: 'yamlValue' }
|
|
101
|
+
const jsonContent = { key: 'jsonValue' }
|
|
102
|
+
|
|
103
|
+
await fse.writeFile(path.join(rootDir, 'config.yaml'), 'key: yamlValue')
|
|
104
|
+
await fse.writeFile(path.join(rootDir, 'config.json'), JSON.stringify(jsonContent))
|
|
105
|
+
|
|
106
|
+
const configs = await loadConfig('*', rootDir)
|
|
107
|
+
expect(configs).toEqual({
|
|
108
|
+
config: yamlContent
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('应该支持多个配置查询,条件为 **/*', async () => {
|
|
113
|
+
const rootDir = await tmp('config')
|
|
114
|
+
const yamlContent = { key: 'yamlValue' }
|
|
115
|
+
const jsonContent = { key: 'jsonValue' }
|
|
116
|
+
|
|
117
|
+
const subDir = path.join(rootDir, 'subdir')
|
|
118
|
+
await fse.ensureDir(subDir)
|
|
119
|
+
|
|
120
|
+
await fse.writeFile(path.join(rootDir, 'config.yaml'), 'key: yamlValue')
|
|
121
|
+
await fse.writeFile(path.join(subDir, 'config.json'), JSON.stringify(jsonContent))
|
|
122
|
+
|
|
123
|
+
const configs = await loadConfig('**/*', rootDir)
|
|
124
|
+
expect(configs).toEqual({
|
|
125
|
+
config: yamlContent,
|
|
126
|
+
'subdir/config': jsonContent
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
|
|
3
|
+
export function filterOneDot(files) {
|
|
4
|
+
return files.filter(file => {
|
|
5
|
+
const base = path.basename(file)
|
|
6
|
+
return /^[^.]+\.[^.]+$/.test(base)
|
|
7
|
+
})
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function filterDoubleDot(files) {
|
|
11
|
+
return files.filter(file => {
|
|
12
|
+
const base = path.basename(file)
|
|
13
|
+
return /^[^.]+\.[^.]+\.[^.]+$/.test(base)
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getRelativePath(absDirectory, file) {
|
|
18
|
+
absDirectory = path.normalize(absDirectory)
|
|
19
|
+
file = path.normalize(file)
|
|
20
|
+
|
|
21
|
+
let relative = file.replace(absDirectory, '')
|
|
22
|
+
relative = relative.split('.')[0]
|
|
23
|
+
relative = relative.replace(/\\/g, '/')
|
|
24
|
+
relative = relative.replace(/^\//, '')
|
|
25
|
+
return relative
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function joinPath(...paths) {
|
|
29
|
+
if (paths.length === 0) return ''
|
|
30
|
+
|
|
31
|
+
const normalizedPaths = paths.map(p => path.normalize(p))
|
|
32
|
+
let result = normalizedPaths[0]
|
|
33
|
+
|
|
34
|
+
for (let i = 1; i < normalizedPaths.length; i++) {
|
|
35
|
+
if (path.isAbsolute(normalizedPaths[i])) {
|
|
36
|
+
result = normalizedPaths[i]
|
|
37
|
+
} else {
|
|
38
|
+
result = path.join(result, normalizedPaths[i])
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return result
|
|
43
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals'
|
|
2
|
+
import { filterOneDot, filterDoubleDot, getRelativePath, joinPath } from './pathUtils.js'
|
|
3
|
+
|
|
4
|
+
describe('Utility Functions', () => {
|
|
5
|
+
it('过滤出包含一个点的文件', () => {
|
|
6
|
+
const files = ['demo.yml', 'demo.zh-CN.yml', 'demo.en.json', 'demo.en.json.bak']
|
|
7
|
+
const filteredFiles = filterOneDot(files)
|
|
8
|
+
expect(filteredFiles.length).toBe(1)
|
|
9
|
+
expect(filteredFiles).toContain('demo.yml')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('过滤出包含两个点的文件', () => {
|
|
13
|
+
const files = ['demo.yml', 'demo.zh-CN.yml', 'demo.en.json', 'demo.en.json.bak']
|
|
14
|
+
const filteredFiles = filterDoubleDot(files)
|
|
15
|
+
expect(filteredFiles.length).toBe(2)
|
|
16
|
+
expect(filteredFiles).toContain('demo.en.json')
|
|
17
|
+
expect(filteredFiles).toContain('demo.zh-CN.yml')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('获取相对路径', () => {
|
|
21
|
+
const absDirectory = '/Users/demo'
|
|
22
|
+
const file = '/Users/demo/tmp/i18n/registry1/demo.yml'
|
|
23
|
+
const relativePath = getRelativePath(absDirectory, file)
|
|
24
|
+
expect(relativePath).toBe('tmp/i18n/registry1/demo')
|
|
25
|
+
|
|
26
|
+
// 支持windows
|
|
27
|
+
const absDirectoryWin = 'C:\\Users\\demo'
|
|
28
|
+
const fileWin = 'C:\\Users\\demo\\tmp\\i18n\\registry1\\demo.yml'
|
|
29
|
+
const relativePathWin = getRelativePath(absDirectoryWin, fileWin)
|
|
30
|
+
expect(relativePathWin).toBe('tmp/i18n/registry1/demo')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('拼接路径 - 空路径数组', () => {
|
|
34
|
+
expect(joinPath()).toBe('')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('拼接路径 - 多路径拼接', () => {
|
|
38
|
+
if (process.platform === 'darwin' || process.platform === 'linux') {
|
|
39
|
+
const path1 = '/Users/demo'
|
|
40
|
+
const path2 = 'tmp/i18n'
|
|
41
|
+
const path3 = 'registry1/demo.yml'
|
|
42
|
+
expect(joinPath(path1, path2, path3)).toBe('/Users/demo/tmp/i18n/registry1/demo.yml')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (process.platform === 'win32') {
|
|
46
|
+
const path1 = 'C:\\Users\\demo'
|
|
47
|
+
const path2 = 'tmp\\i18n'
|
|
48
|
+
const path3 = 'registry1\\demo.yml'
|
|
49
|
+
expect(joinPath(path1, path2, path3)).toBe('C:\\Users\\demo\\tmp\\i18n\\registry1\\demo.yml')
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export default value => {
|
|
2
|
+
let type = typeof value
|
|
3
|
+
if (value === null) {
|
|
4
|
+
type = 'null'
|
|
5
|
+
} else if (type === 'object') {
|
|
6
|
+
const objType = Object.prototype.toString.call(value)
|
|
7
|
+
if (objType === '[object Array]') {
|
|
8
|
+
type = 'array'
|
|
9
|
+
} else if (objType === '[object RegExp]') {
|
|
10
|
+
type = 'regexp'
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return type
|
|
14
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, test, expect } from '@jest/globals'
|
|
2
|
+
import type from './type.js'
|
|
3
|
+
|
|
4
|
+
describe('type 函数', () => {
|
|
5
|
+
test('应该返回 "null" 对于 null', () => {
|
|
6
|
+
expect(type(null)).toBe('null')
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
test('应该返回 "array" 对于数组', () => {
|
|
10
|
+
expect(type([])).toBe('array')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('应该返回 "regexp" 对于正则表达式', () => {
|
|
14
|
+
expect(type(/abc/)).toBe('regexp')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('应该返回 "object" 对于对象', () => {
|
|
18
|
+
expect(type({})).toBe('object')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('应该返回 "string" 对于字符串', () => {
|
|
22
|
+
expect(type('hello')).toBe('string')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('应该返回 "number" 对于数字', () => {
|
|
26
|
+
expect(type(123)).toBe('number')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('应该返回 "boolean" 对于布尔值', () => {
|
|
30
|
+
expect(type(true)).toBe('boolean')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('应该返回 "undefined" 对于 undefined', () => {
|
|
34
|
+
expect(type(undefined)).toBe('undefined')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('应该返回 "function" 对于函数', () => {
|
|
38
|
+
expect(type(() => {})).toBe('function')
|
|
39
|
+
})
|
|
40
|
+
})
|
package/package.json
CHANGED
|
@@ -1 +1,61 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "sumor",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"main": "src/index.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sumor": "src/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"demo": "cd demo && npm run start",
|
|
11
|
+
"check": "npm run autofix && npm run coverage",
|
|
12
|
+
"codeTest": "node test-utils/codeTest/index.js",
|
|
13
|
+
"=============devops=============": "",
|
|
14
|
+
"lint": "eslint .",
|
|
15
|
+
"autofix": "eslint --fix . && prettier --write . && npm audit fix --force",
|
|
16
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testMatch='**/*.test.js'",
|
|
17
|
+
"coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --testMatch='**/*.test.js'",
|
|
18
|
+
"=============automation=============": "",
|
|
19
|
+
"prepare": "husky",
|
|
20
|
+
"pre-commit": "npm run autofix",
|
|
21
|
+
"pre-push": "npm run test",
|
|
22
|
+
"=============publish=============": "",
|
|
23
|
+
"push-tag": "git push && git push --tags",
|
|
24
|
+
"publish-beta": "npm version prerelease --preid beta && npm run push-tag",
|
|
25
|
+
"publish-release-patch": "npm version patch && npm run push-tag",
|
|
26
|
+
"publish-release-minor": "npm version minor && npm run push-tag",
|
|
27
|
+
"publish-release-major": "npm version major && npm run push-tag"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [],
|
|
30
|
+
"author": "",
|
|
31
|
+
"license": "UNLICENSED",
|
|
32
|
+
"description": "",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"axios": "^1.8.4",
|
|
35
|
+
"body-parser": "^2.2.0",
|
|
36
|
+
"chalk": "^5.4.1",
|
|
37
|
+
"commander": "^13.1.0",
|
|
38
|
+
"express": "^4.21.2",
|
|
39
|
+
"fs-extra": "^11.3.0",
|
|
40
|
+
"glob": "^11.0.1",
|
|
41
|
+
"multer": "^1.4.5-lts.2",
|
|
42
|
+
"os-locale": "^6.0.2",
|
|
43
|
+
"serve-static": "^2.2.0",
|
|
44
|
+
"swagger-ui-express": "^5.0.1",
|
|
45
|
+
"yaml": "^2.7.1"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@jest/globals": "^29.7.0",
|
|
49
|
+
"eslint": "^8.57.0",
|
|
50
|
+
"eslint-config-prettier": "^9.1.0",
|
|
51
|
+
"eslint-config-standard": "^17.1.0",
|
|
52
|
+
"eslint-plugin-prettier": "^5.1.3",
|
|
53
|
+
"form-data": "^4.0.2",
|
|
54
|
+
"husky": "^9.0.11",
|
|
55
|
+
"inquirer": "^12.4.2",
|
|
56
|
+
"jest": "^29.7.0",
|
|
57
|
+
"jest-html-reporter": "^3.10.2",
|
|
58
|
+
"portfinder": "^1.0.33",
|
|
59
|
+
"prettier": "^3.2.5"
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/app.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import loadConfig from '../modules/utils/loadConfig.js'
|
|
2
|
+
import serve from '../modules/serve/index.js'
|
|
3
|
+
import i18nMiddleware from '../modules/middlewares/i18nMiddleware/index.js'
|
|
4
|
+
import { cache } from '../modules/i18n/registry.js'
|
|
5
|
+
import apiMiddleware from '../modules/middlewares/apiMiddleware/index.js'
|
|
6
|
+
import tokenMiddleware from '../modules/middlewares/tokenMiddleware/index.js'
|
|
7
|
+
|
|
8
|
+
export default async configPath => {
|
|
9
|
+
configPath = configPath || `${process.cwd()}/config/config`
|
|
10
|
+
const userConfig = await loadConfig(configPath)
|
|
11
|
+
|
|
12
|
+
const initApp = app => {
|
|
13
|
+
const { logger } = app.namespace('sumor_app')
|
|
14
|
+
logger.info('APP_STARTING')
|
|
15
|
+
|
|
16
|
+
for (const namespace in cache) {
|
|
17
|
+
logger.debug('I18N_REGISTERED', { namespace: namespace.toUpperCase() })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 记录HTTP请求
|
|
21
|
+
app.use(async (req, res, next) => {
|
|
22
|
+
const { logger } = req.namespace('SUMOR_HTTP')
|
|
23
|
+
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || '0.0.0.0'
|
|
24
|
+
const agent = req.headers['user-agent'] || 'unknown agent'
|
|
25
|
+
logger.trace(`${req.method} ${req.originalUrl} IP/${ip} ${agent}`)
|
|
26
|
+
next()
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { app, config } = await serve(userConfig, [
|
|
31
|
+
i18nMiddleware,
|
|
32
|
+
tokenMiddleware,
|
|
33
|
+
initApp,
|
|
34
|
+
apiMiddleware(`${process.cwd()}/api`)
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
const { logger } = app.namespace('sumor_app')
|
|
38
|
+
|
|
39
|
+
logger.info('APP_STARTED', { port: config.port })
|
|
40
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fse from 'fs-extra'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import { Command } from 'commander'
|
|
6
|
+
import { fileURLToPath } from 'url'
|
|
7
|
+
|
|
8
|
+
// modules
|
|
9
|
+
import app from './app.js'
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
12
|
+
const __dirname = path.dirname(__filename)
|
|
13
|
+
|
|
14
|
+
const packageJsonPath = path.resolve(__dirname, '../package.json')
|
|
15
|
+
const packageJson = await fse.readJSON(packageJsonPath)
|
|
16
|
+
|
|
17
|
+
const program = new Command()
|
|
18
|
+
|
|
19
|
+
program.name('sumor-cli').description('轻呈云应用命令行工具').version(packageJson.version)
|
|
20
|
+
|
|
21
|
+
program
|
|
22
|
+
.command('start')
|
|
23
|
+
.description('启动轻呈云应用')
|
|
24
|
+
.action(() => {
|
|
25
|
+
app()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
program.parse(process.argv)
|
package/src/index.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/* 当浏览器设置为dark模式时生效 */
|
|
2
|
+
a {
|
|
3
|
+
color: #2196f3;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
@media (prefers-color-scheme: dark) {
|
|
7
|
+
body {
|
|
8
|
+
background: #000;
|
|
9
|
+
color: #fff;
|
|
10
|
+
}
|
|
11
|
+
.quiet {
|
|
12
|
+
color: rgba(255, 255, 255, 0.5);
|
|
13
|
+
}
|
|
14
|
+
.low {
|
|
15
|
+
background: rgba(255, 87, 34, 0.5);
|
|
16
|
+
}
|
|
17
|
+
.high {
|
|
18
|
+
background: rgba(255, 255, 255, 0.1);
|
|
19
|
+
}
|
|
20
|
+
.medium {
|
|
21
|
+
background: rgba(255, 87, 34, 0.3);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.status-line.medium,
|
|
25
|
+
.medium .cover-fill {
|
|
26
|
+
background: rgb(255, 87, 34);
|
|
27
|
+
}
|
|
28
|
+
a {
|
|
29
|
+
color: #a9d9ff;
|
|
30
|
+
}
|
|
31
|
+
.coverage-summary th {
|
|
32
|
+
background: rgba(255, 255, 255, 0.3);
|
|
33
|
+
}
|
|
34
|
+
thead tr {
|
|
35
|
+
border: 1px solid #bbb;
|
|
36
|
+
}
|
|
37
|
+
pre {
|
|
38
|
+
background: rgba(255, 255, 255, 1);
|
|
39
|
+
color: #000;
|
|
40
|
+
.quiet {
|
|
41
|
+
color: rgba(0, 0, 0, 0.7);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
pre a {
|
|
45
|
+
color: #2196f3;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import inquirer from 'inquirer'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import fse from 'fs-extra'
|
|
4
|
+
import getTestFiles from './utils/getTestFiles.js'
|
|
5
|
+
import runTests from './utils/runTests.js'
|
|
6
|
+
import startServer from './utils/startServer.js'
|
|
7
|
+
const LAST_TEST_FILE_PATH = path.join(process.cwd(), 'tmp', 'tools', 'codeTest', 'lastTestFile.txt')
|
|
8
|
+
|
|
9
|
+
// 主函数
|
|
10
|
+
const executeCodeTest = async () => {
|
|
11
|
+
const testFiles = getTestFiles(path.join(process.cwd(), 'modules'))
|
|
12
|
+
if (testFiles.length === 0) {
|
|
13
|
+
console.log('未找到测试文件。')
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// const sortedTestFiles = testFiles.sort((a, b) => {
|
|
18
|
+
// if (a.endsWith('/index.test.js') && !b.endsWith('/index.test.js')) return -1
|
|
19
|
+
// if (!a.endsWith('/index.test.js') && b.endsWith('/index.test.js')) return 1
|
|
20
|
+
// return a.localeCompare(b)
|
|
21
|
+
// })
|
|
22
|
+
|
|
23
|
+
let displayTestFiles = testFiles.map(file => {
|
|
24
|
+
if (file.endsWith('/index.test.js'))
|
|
25
|
+
return {
|
|
26
|
+
name: file.replace('/index.test.js', ''),
|
|
27
|
+
value: file
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
name: file.replace('.test.js', ''),
|
|
31
|
+
value: file
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
displayTestFiles = displayTestFiles.sort((a, b) => {
|
|
35
|
+
if (b.name > a.name) return -1
|
|
36
|
+
else return 1
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
let lastTestFile = ''
|
|
40
|
+
if (await fse.exists(LAST_TEST_FILE_PATH)) {
|
|
41
|
+
lastTestFile = await fse.readFile(LAST_TEST_FILE_PATH, 'utf-8')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const answers = await inquirer.prompt([
|
|
45
|
+
{
|
|
46
|
+
type: 'list',
|
|
47
|
+
name: 'file',
|
|
48
|
+
message: `选择要运行的测试文件:(共${displayTestFiles.length - 1}个)`,
|
|
49
|
+
choices: [{ name: `全部`, value: '' }, ...displayTestFiles],
|
|
50
|
+
default: lastTestFile
|
|
51
|
+
}
|
|
52
|
+
])
|
|
53
|
+
|
|
54
|
+
let selectedFile = answers.file
|
|
55
|
+
selectedFile = selectedFile || ''
|
|
56
|
+
const report = await runTests(LAST_TEST_FILE_PATH, selectedFile)
|
|
57
|
+
|
|
58
|
+
const unitPath = path.join(report, 'unit')
|
|
59
|
+
|
|
60
|
+
const generatedUnitPath = path.join(process.cwd(), 'output', 'unit')
|
|
61
|
+
await fse.move(generatedUnitPath, path.join(process.cwd(), unitPath))
|
|
62
|
+
|
|
63
|
+
const unitPort = await startServer(path.join(process.cwd(), unitPath))
|
|
64
|
+
console.log(`单元测试报告请访问 http://localhost:${unitPort}`)
|
|
65
|
+
// console.log(` - 报告目录:${unitPath}`)
|
|
66
|
+
|
|
67
|
+
const coveragePath = path.join(report, 'coverage', 'lcov-report')
|
|
68
|
+
|
|
69
|
+
// 将custom.css的内容追加到base.css文件中
|
|
70
|
+
const customCssPath = path.join(new URL('.', import.meta.url).pathname, 'custom.css')
|
|
71
|
+
const baseCssPath = path.join(coveragePath, 'base.css')
|
|
72
|
+
const customCss = await fse.readFile(customCssPath, 'utf-8')
|
|
73
|
+
await fse.appendFile(baseCssPath, customCss)
|
|
74
|
+
|
|
75
|
+
const coveragePort = await startServer(path.join(process.cwd(), coveragePath))
|
|
76
|
+
console.log(`覆盖率报告请访问 http://localhost:${coveragePort}`)
|
|
77
|
+
// console.log(` - 报告目录:${coveragePath}`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
executeCodeTest()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export function calculateCoverage(coverageJSON) {
|
|
2
|
+
const results = {}
|
|
3
|
+
let totalStatements = 0
|
|
4
|
+
let totalFunctions = 0
|
|
5
|
+
let totalBranches = 0
|
|
6
|
+
let totalStatementCoverage = 0
|
|
7
|
+
let totalFunctionCoverage = 0
|
|
8
|
+
let totalBranchCoverage = 0
|
|
9
|
+
|
|
10
|
+
for (const filePath in coverageJSON) {
|
|
11
|
+
const fileCoverage = coverageJSON[filePath]
|
|
12
|
+
const statements = Object.keys(fileCoverage.s).length
|
|
13
|
+
const functions = Object.keys(fileCoverage.f).length
|
|
14
|
+
const branches = Object.keys(fileCoverage.b).length
|
|
15
|
+
const statementCoverage =
|
|
16
|
+
(Object.values(fileCoverage.s).filter(v => v > 0).length / statements) * 100
|
|
17
|
+
const functionCoverage =
|
|
18
|
+
(Object.values(fileCoverage.f).filter(v => v > 0).length / functions) * 100
|
|
19
|
+
const branchCoverage =
|
|
20
|
+
(Object.values(fileCoverage.b).filter(v => v.every(b => b > 0)).length / branches) * 100
|
|
21
|
+
|
|
22
|
+
results[filePath] = {
|
|
23
|
+
statements,
|
|
24
|
+
functions,
|
|
25
|
+
branches,
|
|
26
|
+
statementCoverage,
|
|
27
|
+
functionCoverage,
|
|
28
|
+
branchCoverage
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
totalStatements += statements
|
|
32
|
+
totalFunctions += functions
|
|
33
|
+
totalBranches += branches
|
|
34
|
+
if (statements > 0) {
|
|
35
|
+
totalStatementCoverage += statementCoverage * statements
|
|
36
|
+
}
|
|
37
|
+
if (functions > 0) {
|
|
38
|
+
totalFunctionCoverage += functionCoverage * functions
|
|
39
|
+
}
|
|
40
|
+
if (branches > 0) {
|
|
41
|
+
totalBranchCoverage += branchCoverage * branches
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const total = {
|
|
46
|
+
statements: totalStatements,
|
|
47
|
+
functions: totalFunctions,
|
|
48
|
+
branches: totalBranches,
|
|
49
|
+
statementCoverage: totalStatementCoverage / totalStatements,
|
|
50
|
+
functionCoverage: totalFunctionCoverage / totalFunctions,
|
|
51
|
+
branchCoverage: totalBranchCoverage / totalBranches
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { results, total }
|
|
55
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
// 获取当前目录下的所有测试文件的函数
|
|
5
|
+
const getTestFiles = testFolder => {
|
|
6
|
+
let testFiles = []
|
|
7
|
+
const files = fs.readdirSync(testFolder)
|
|
8
|
+
files.forEach(file => {
|
|
9
|
+
const filePath = path.join(testFolder, file)
|
|
10
|
+
const stat = fs.statSync(filePath)
|
|
11
|
+
if (stat.isDirectory()) {
|
|
12
|
+
testFiles = testFiles.concat(getTestFiles(filePath))
|
|
13
|
+
} else if (file.endsWith('.test.js')) {
|
|
14
|
+
testFiles.push(filePath.replace(process.cwd() + path.sep, ''))
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
return testFiles
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default getTestFiles
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { execSync } from 'child_process'
|
|
2
|
+
import fse from 'fs-extra'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
// 替换 .test.ts 为 .test.js
|
|
6
|
+
const runTests = async (config, file) => {
|
|
7
|
+
await fse.remove(path.join(process.cwd(), 'tmp', 'test'))
|
|
8
|
+
|
|
9
|
+
await fse.ensureFile(config)
|
|
10
|
+
await fse.writeFile(config, file, 'utf-8')
|
|
11
|
+
|
|
12
|
+
let folder = 'all'
|
|
13
|
+
if (file !== '') {
|
|
14
|
+
folder = file.replace('.test.js', '').replace(/\/|\\/g, '.').replace('.index', '')
|
|
15
|
+
}
|
|
16
|
+
const root = `./output/test/${folder}`
|
|
17
|
+
await fse.remove(path.join(process.cwd(), root))
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const cmdArr = [
|
|
21
|
+
'node --experimental-vm-modules node_modules/jest/bin/jest.js',
|
|
22
|
+
file.replace(/\\/g, '/'),
|
|
23
|
+
'--coverage',
|
|
24
|
+
'--coverageDirectory=' + root + '/coverage'
|
|
25
|
+
]
|
|
26
|
+
console.log(cmdArr.join(' '))
|
|
27
|
+
execSync(cmdArr.join(' '), {
|
|
28
|
+
stdio: 'inherit'
|
|
29
|
+
})
|
|
30
|
+
} catch (e) {
|
|
31
|
+
if (e) {
|
|
32
|
+
console.log('\n单元测试失败,调试请查看上述日志')
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return root
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default runTests
|