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.
Files changed (179) hide show
  1. package/.eslintignore +6 -0
  2. package/.eslintrc.yml +4 -0
  3. package/.gitattributes +1 -0
  4. package/.github/workflows/audit.yml +22 -0
  5. package/.github/workflows/available.yml +22 -0
  6. package/.github/workflows/ci.yml +45 -0
  7. package/.github/workflows/coverage.yml +23 -0
  8. package/.github/workflows/publish.yml +31 -0
  9. package/.github/workflows/ut.yml +24 -0
  10. package/.husky/pre-commit +10 -0
  11. package/.husky/pre-push +10 -0
  12. package/.prettierrc.yml +7 -0
  13. package/LICENSE +21 -0
  14. package/demo/api/error.js +3 -0
  15. package/demo/api/file.js +4 -0
  16. package/demo/api/file.yaml +11 -0
  17. package/demo/api/hello.js +4 -0
  18. package/demo/api/hello.yaml +13 -0
  19. package/demo/api/login.js +4 -0
  20. package/demo/api/logout.js +4 -0
  21. package/demo/api/long.js +9 -0
  22. package/demo/api/long.yaml +2 -0
  23. package/demo/api/user/admin.js +12 -0
  24. package/demo/api/user/info.js +8 -0
  25. package/demo/config/config.yaml +2 -0
  26. package/demo/i18n/demo.yaml +1 -0
  27. package/demo/package.json +6 -0
  28. package/i18n/README.md +32 -0
  29. package/i18n/sumor_api.yaml +16 -0
  30. package/i18n/sumor_app.yaml +3 -0
  31. package/i18n/sumor_internal.yaml +1 -0
  32. package/i18n/sumor_token.yaml +2 -0
  33. package/i18nExt/sumor_api.de.yaml +3 -0
  34. package/i18nExt/sumor_api.en.yaml +3 -0
  35. package/i18nExt/sumor_api.es.yaml +3 -0
  36. package/i18nExt/sumor_api.fr.yaml +3 -0
  37. package/i18nExt/sumor_api.it.yaml +3 -0
  38. package/i18nExt/sumor_api.ja.yaml +3 -0
  39. package/i18nExt/sumor_api.ko.yaml +3 -0
  40. package/i18nExt/sumor_api.pt.yaml +3 -0
  41. package/i18nExt/sumor_api.zh-TW.yaml +3 -0
  42. package/i18nExt/sumor_api.zh.yaml +3 -0
  43. package/i18nExt/sumor_app.de.yaml +3 -0
  44. package/i18nExt/sumor_app.en.yaml +3 -0
  45. package/i18nExt/sumor_app.es.yaml +3 -0
  46. package/i18nExt/sumor_app.zh-TW.yaml +3 -0
  47. package/i18nExt/sumor_app.zh.yaml +3 -0
  48. package/i18nExt/sumor_internal.de.yaml +1 -0
  49. package/i18nExt/sumor_internal.en.yaml +1 -0
  50. package/i18nExt/sumor_internal.es.yaml +1 -0
  51. package/i18nExt/sumor_internal.zh-TW.yaml +1 -0
  52. package/i18nExt/sumor_internal.zh.yaml +1 -0
  53. package/i18nExt/sumor_token.de.yaml +2 -0
  54. package/i18nExt/sumor_token.es.yaml +2 -0
  55. package/i18nExt/sumor_token.fr.yaml +2 -0
  56. package/i18nExt/sumor_token.it.yaml +2 -0
  57. package/i18nExt/sumor_token.ja.yaml +2 -0
  58. package/i18nExt/sumor_token.ko.yaml +2 -0
  59. package/i18nExt/sumor_token.pt.yaml +2 -0
  60. package/i18nExt/sumor_token.ru.yaml +2 -0
  61. package/i18nExt/sumor_token.tr.yaml +2 -0
  62. package/i18nExt/sumor_token.zh-TW.yaml +2 -0
  63. package/i18nExt/sumor_token.zh.yaml +2 -0
  64. package/jest.config.json +26 -0
  65. package/modules/alertPage/html/template.html +162 -0
  66. package/modules/alertPage/html/undraw_access-denied.svg +52 -0
  67. package/modules/alertPage/html/undraw_alert.svg +1 -0
  68. package/modules/alertPage/html/undraw_celebration.svg +78 -0
  69. package/modules/alertPage/html/undraw_complete-form.svg +1 -0
  70. package/modules/alertPage/html/undraw_login.svg +1 -0
  71. package/modules/alertPage/index.js +46 -0
  72. package/modules/alertPage/index.test.js +12 -0
  73. package/modules/i18n/README.md +90 -0
  74. package/modules/i18n/convertI18nValue/README.md +31 -0
  75. package/modules/i18n/convertI18nValue/getI18nTemplate.js +26 -0
  76. package/modules/i18n/convertI18nValue/getI18nTemplate.test.js +40 -0
  77. package/modules/i18n/convertI18nValue/index.js +14 -0
  78. package/modules/i18n/convertI18nValue/index.test.js +55 -0
  79. package/modules/i18n/convertI18nValue/stringVariableReplace.js +13 -0
  80. package/modules/i18n/convertI18nValue/stringVariableReplace.test.js +39 -0
  81. package/modules/i18n/index.js +26 -0
  82. package/modules/i18n/index.test.js +13 -0
  83. package/modules/i18n/load/README.md +28 -0
  84. package/modules/i18n/load/load.js +31 -0
  85. package/modules/i18n/load/load.test.js +30 -0
  86. package/modules/i18n/registry.js +48 -0
  87. package/modules/i18n/registry.test.js +84 -0
  88. package/modules/logger/convert/parseFile.js +14 -0
  89. package/modules/logger/convert/parseFile.test.js +28 -0
  90. package/modules/logger/convert/stringifyCMD.js +48 -0
  91. package/modules/logger/convert/stringifyCMD.test.js +37 -0
  92. package/modules/logger/convert/stringifyFile.js +24 -0
  93. package/modules/logger/convert/stringifyFile.test.js +79 -0
  94. package/modules/logger/index.js +82 -0
  95. package/modules/logger/index.test.js +124 -0
  96. package/modules/logger/logFileOperator.js +50 -0
  97. package/modules/logger/logFileOperator.test.js +69 -0
  98. package/modules/middlewares/apiMiddleware/errorCatcher.js +9 -0
  99. package/modules/middlewares/apiMiddleware/exposeApis/index.js +82 -0
  100. package/modules/middlewares/apiMiddleware/index.js +111 -0
  101. package/modules/middlewares/apiMiddleware/index.test.js +145 -0
  102. package/modules/middlewares/apiMiddleware/load/index.js +35 -0
  103. package/modules/middlewares/apiMiddleware/load/index.test.js +30 -0
  104. package/modules/middlewares/apiMiddleware/metadataToSwagger.js +139 -0
  105. package/modules/middlewares/apiMiddleware/prepareData/format/caseSensitive.js +26 -0
  106. package/modules/middlewares/apiMiddleware/prepareData/format/caseSensitive.test.js +36 -0
  107. package/modules/middlewares/apiMiddleware/prepareData/format/convertType.js +48 -0
  108. package/modules/middlewares/apiMiddleware/prepareData/format/convertType.test.js +53 -0
  109. package/modules/middlewares/apiMiddleware/prepareData/format/defaultValue.js +31 -0
  110. package/modules/middlewares/apiMiddleware/prepareData/format/defaultValue.test.js +31 -0
  111. package/modules/middlewares/apiMiddleware/prepareData/format/index.js +18 -0
  112. package/modules/middlewares/apiMiddleware/prepareData/format/index.test.js +40 -0
  113. package/modules/middlewares/apiMiddleware/prepareData/format/precision.js +12 -0
  114. package/modules/middlewares/apiMiddleware/prepareData/format/precision.test.js +33 -0
  115. package/modules/middlewares/apiMiddleware/prepareData/format/trim.js +15 -0
  116. package/modules/middlewares/apiMiddleware/prepareData/format/trim.test.js +24 -0
  117. package/modules/middlewares/apiMiddleware/prepareData/index.js +29 -0
  118. package/modules/middlewares/apiMiddleware/prepareData/index.test.js +121 -0
  119. package/modules/middlewares/apiMiddleware/prepareData/validate/checkLength.js +26 -0
  120. package/modules/middlewares/apiMiddleware/prepareData/validate/checkLength.test.js +52 -0
  121. package/modules/middlewares/apiMiddleware/prepareData/validate/index.js +30 -0
  122. package/modules/middlewares/apiMiddleware/public/favicon.ico +0 -0
  123. package/modules/middlewares/apiMiddleware/response/sendError.js +57 -0
  124. package/modules/middlewares/apiMiddleware/response/sendError.test.js +251 -0
  125. package/modules/middlewares/apiMiddleware/response/sendNotFound.js +26 -0
  126. package/modules/middlewares/apiMiddleware/response/sendResponse.js +25 -0
  127. package/modules/middlewares/apiMiddleware/response/sendSuccess.js +30 -0
  128. package/modules/middlewares/bodyMiddleware/cleanupFiles.js +14 -0
  129. package/modules/middlewares/bodyMiddleware/cleanupFiles.test.js +54 -0
  130. package/modules/middlewares/bodyMiddleware/fileParser.js +69 -0
  131. package/modules/middlewares/bodyMiddleware/fileParser.test.js +163 -0
  132. package/modules/middlewares/bodyMiddleware/index.js +12 -0
  133. package/modules/middlewares/bodyMiddleware/index.test.js +64 -0
  134. package/modules/middlewares/bodyMiddleware/mergeData.js +4 -0
  135. package/modules/middlewares/bodyMiddleware/mergeData.test.js +38 -0
  136. package/modules/middlewares/i18nMiddleware/index.js +82 -0
  137. package/modules/middlewares/i18nMiddleware/index.test.js +75 -0
  138. package/modules/middlewares/tokenMiddleware/Token.js +115 -0
  139. package/modules/middlewares/tokenMiddleware/Token.test.js +67 -0
  140. package/modules/middlewares/tokenMiddleware/index.js +32 -0
  141. package/modules/middlewares/tokenMiddleware/index.test.js +115 -0
  142. package/modules/middlewares/tokenMiddleware/parseCookie.js +24 -0
  143. package/modules/middlewares/tokenMiddleware/parseCookie.test.js +49 -0
  144. package/modules/serve/formatConfig.js +7 -0
  145. package/modules/serve/formatConfig.test.js +28 -0
  146. package/modules/serve/index.js +30 -0
  147. package/modules/serve/index.test.js +69 -0
  148. package/modules/serve/listenApp.js +11 -0
  149. package/modules/system/getSystemLanguage.js +9 -0
  150. package/modules/system/getSystemLanguage.test.js +19 -0
  151. package/modules/utils/getError.js +56 -0
  152. package/modules/utils/getError.test.js +97 -0
  153. package/modules/utils/loadConfig.js +73 -0
  154. package/modules/utils/loadConfig.test.js +129 -0
  155. package/modules/utils/pathUtils.js +43 -0
  156. package/modules/utils/pathUtils.test.js +52 -0
  157. package/modules/utils/type.js +14 -0
  158. package/modules/utils/type.test.js +40 -0
  159. package/package.json +61 -1
  160. package/src/app.js +40 -0
  161. package/src/cli.js +28 -0
  162. package/src/index.js +3 -0
  163. package/test-utils/codeTest/README.md +4 -0
  164. package/test-utils/codeTest/custom.css +47 -0
  165. package/test-utils/codeTest/index.js +80 -0
  166. package/test-utils/codeTest/utils/calculateCoverage.js +55 -0
  167. package/test-utils/codeTest/utils/getTestFiles.js +20 -0
  168. package/test-utils/codeTest/utils/runTests.js +38 -0
  169. package/test-utils/codeTest/utils/startServer.js +23 -0
  170. package/test-utils/tmp.js +15 -0
  171. package/cli.js +0 -3
  172. package/index.es.js +0 -578
  173. package/template/web/AppWithFrame.vue +0 -20
  174. package/template/web/index.html +0 -19
  175. package/template/web/src/App.vue +0 -15
  176. package/template/web/src/entry-client.js +0 -9
  177. package/template/web/src/entry-server.js +0 -69
  178. package/template/web/src/main.js +0 -41
  179. package/template/web/src/style.scss +0 -389
@@ -0,0 +1,14 @@
1
+ import getI18nValue from './getI18nTemplate.js'
2
+ import stringVariableReplace from './stringVariableReplace.js'
3
+
4
+ export default function (config, target, code, data) {
5
+ const i18nTemplate = getI18nValue(config, target, code)
6
+ if (i18nTemplate === undefined) {
7
+ return code
8
+ }
9
+ if (typeof i18nTemplate === 'string') {
10
+ return stringVariableReplace(i18nTemplate, data)
11
+ } else {
12
+ return i18nTemplate
13
+ }
14
+ }
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect } from '@jest/globals'
2
+ import convertI18n from './index.js'
3
+
4
+ describe('convertI18n', () => {
5
+ const config = {
6
+ en: { greeting: 'Hello, {name}!' },
7
+ 'zh-CN': { greeting: '你好, {name}!' },
8
+ origin: { greeting: 'Hi, {name}!' }
9
+ }
10
+ const config2 = {
11
+ 'zh-CN': { greeting: '你好, {name}!' },
12
+ origin: { greeting: 'Hi, {name}!' }
13
+ }
14
+ const config3 = {
15
+ 'zh-CN': { allowRegister: true },
16
+ origin: { allowRegister: false }
17
+ }
18
+
19
+ it('应该替换模板中的变量', () => {
20
+ const data = { name: 'John' }
21
+ const result = convertI18n(config, 'en', 'greeting', data)
22
+ expect(result).toBe('Hello, John!')
23
+ })
24
+
25
+ it('应该返回指定区域的模板', () => {
26
+ const data = { name: '李雷' }
27
+ const result = convertI18n(config, 'zh-CN', 'greeting', data)
28
+ expect(result).toBe('你好, 李雷!')
29
+ })
30
+
31
+ it('如果找不到区域,应该返回原始模板', () => {
32
+ const data = { name: 'Jane' }
33
+ const result = convertI18n(config, 'fr', 'greeting', data)
34
+ expect(result).toBe('Hello, Jane!')
35
+ })
36
+
37
+ it('如果找不到区域且找不到en,应该返回原始模板', () => {
38
+ const data = { name: 'Jane' }
39
+ const result = convertI18n(config2, 'fr', 'greeting', data)
40
+ expect(result).toBe('Hi, Jane!')
41
+ })
42
+
43
+ it('如果类型不是字符串,应该返回原始模板', () => {
44
+ const result1 = convertI18n(config3, 'fr', 'allowRegister', {})
45
+ expect(result1).toBe(false)
46
+ const result2 = convertI18n(config3, 'zh-CN', 'allowRegister', {})
47
+ expect(result2).toBe(true)
48
+ })
49
+
50
+ it('如果找不到i18n模板,应该返回代码', () => {
51
+ const data = { name: 'John' }
52
+ const result = convertI18n(config, 'en', 'nonexistent', data)
53
+ expect(result).toBe('nonexistent')
54
+ })
55
+ })
@@ -0,0 +1,13 @@
1
+ export default function (template, data) {
2
+ data = data || {}
3
+ return template.replace(/{(\w+)}/g, (_, key) => {
4
+ const value = data[key]
5
+ if (value === undefined || value === null) {
6
+ return '?'
7
+ }
8
+ if (typeof value === 'object') {
9
+ return JSON.stringify(value)
10
+ }
11
+ return String(value)
12
+ })
13
+ }
@@ -0,0 +1,39 @@
1
+ import { describe, test, expect } from '@jest/globals'
2
+ import stringVariableReplace from './stringVariableReplace.js'
3
+
4
+ describe('字符串变量替换工具', () => {
5
+ test('替换占位符为值', () => {
6
+ const template = 'User not found: {name}'
7
+ const data = { name: 'Tester' }
8
+ const result = stringVariableReplace(template, data)
9
+ expect(result).toBe('User not found: Tester')
10
+ })
11
+
12
+ test('如果未提供值,则返回占位符', () => {
13
+ const template = 'User not found: {name}'
14
+ const data = {}
15
+ const result = stringVariableReplace(template, data)
16
+ expect(result).toBe('User not found: ?')
17
+ })
18
+
19
+ test('如果值是对象,则将占位符替换为JSON字符串', () => {
20
+ const template = 'User details: {user}'
21
+ const data = { user: { id: 1, name: 'Tester' } }
22
+ const result = stringVariableReplace(template, data)
23
+ expect(result).toBe('User details: {"id":1,"name":"Tester"}')
24
+ })
25
+
26
+ test('如果值为null,则返回问号', () => {
27
+ const template = 'User not found: {name}'
28
+ const data = { name: null }
29
+ const result = stringVariableReplace(template, data)
30
+ expect(result).toBe('User not found: ?')
31
+ })
32
+
33
+ test('将数字值转换为字符串', () => {
34
+ const template = 'User age: {age}'
35
+ const data = { age: 30 }
36
+ const result = stringVariableReplace(template, data)
37
+ expect(result).toBe('User age: 30')
38
+ })
39
+ })
@@ -0,0 +1,26 @@
1
+ import convertI18nValue from './convertI18nValue/index.js'
2
+ import { cache, register, registerAll } from './registry.js'
3
+ import path from 'path'
4
+
5
+ export { cache as data }
6
+
7
+ export { register, registerAll }
8
+
9
+ // 预定义的 i18n 文件夹路径
10
+ const moduleRoot = new URL('.', import.meta.url).pathname
11
+ const predefinedI18nPath = path.join(moduleRoot, '../../i18n')
12
+ await registerAll(predefinedI18nPath)
13
+ const predefinedI18nExtPath = path.join(moduleRoot, '../../i18nExt')
14
+ await registerAll(predefinedI18nExtPath)
15
+
16
+ const convert = function (namespace, target, code, data = {}) {
17
+ if (!cache[namespace.toLowerCase()]) {
18
+ return
19
+ }
20
+
21
+ const i18nConfig = cache[namespace.toLowerCase()]
22
+
23
+ return convertI18nValue(i18nConfig, target, code, data)
24
+ }
25
+
26
+ export { convert }
@@ -0,0 +1,13 @@
1
+ import { describe, it, expect } from '@jest/globals'
2
+ import { convert } from './index.js'
3
+
4
+ describe('i18n 转换函数', () => {
5
+ it('如果命名空间未注册,返回undefined', () => {
6
+ const result = convert('unregisteredNamespace', 'target', 'code')
7
+ expect(result).toBeUndefined()
8
+ })
9
+ it('检查预定义的 i18n 文件夹路径', () => {
10
+ expect(convert('sumor_internal', 'en', 'TEST', { value: 'value' })).toBe('Test value')
11
+ expect(convert('SUMOR_INTERNAL', 'zh-CN', 'TEST', { value: 'value' })).toBe('测试 value')
12
+ })
13
+ })
@@ -0,0 +1,28 @@
1
+ ## 加载配置
2
+
3
+ load 目录为加载 i18n 配置的目录。将会根据指定目录,加载所有的配置文件。
4
+
5
+ ### 示例
6
+
7
+ 配置目录为 /config/i18n,基础配置文件名为 demo.yml。
8
+ 则调用 load 模块时,传入的参数为`/config/i18n/demo`。
9
+ 会加载如下文件:
10
+
11
+ - /config/i18n/demo.yml
12
+ - /config/i18n/demo.zh-CN.yml
13
+ - /config/i18n/demo.zh.yml
14
+ - /config/i18n/demo.en.yml
15
+ - ... 等地区语言后缀的文件
16
+
17
+ #### 返回值
18
+
19
+ demo.yml 会作为 origin 的值,其他文件会根据语言后缀,作为对应的语言配置。
20
+
21
+ ```json
22
+ {
23
+ "origin": { "key": "ORIGIN" },
24
+ "zh-CN": { "key": "CHINA" },
25
+ "zh": { "key": "CHINESE" },
26
+ "en": { "key": "ENGLISH" }
27
+ }
28
+ ```
@@ -0,0 +1,31 @@
1
+ import loadConfig from '../../utils/loadConfig.js'
2
+ import path from 'path'
3
+
4
+ async function loadI18nConfig(i18nPath) {
5
+ const result = {
6
+ origin: {}
7
+ }
8
+
9
+ // 去除i18nPath后缀
10
+ i18nPath = i18nPath.split('.')[0]
11
+
12
+ const root = path.dirname(i18nPath)
13
+ const name = path.basename(i18nPath)
14
+ const files = await loadConfig(name + '*', root)
15
+
16
+ for (const key in files) {
17
+ // key为name.*才是国际化文件
18
+ const prefix = `${name}.`
19
+
20
+ if (key === name) {
21
+ result.origin = files[key]
22
+ } else if (key.startsWith(prefix)) {
23
+ const lang = key.replace(prefix, '')
24
+ result[lang] = files[key]
25
+ }
26
+ }
27
+
28
+ return result
29
+ }
30
+
31
+ export default loadI18nConfig
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from '@jest/globals'
2
+ import fs from 'fs-extra'
3
+ import path from 'path'
4
+ import loadI18nConfig from './load.js'
5
+ import tmp from '../../../test-utils/tmp.js'
6
+
7
+ const tmpDir = await tmp('i18nLoad')
8
+
9
+ describe('加载国际化数据目录', () => {
10
+ beforeAll(() => {
11
+ fs.ensureDirSync(tmpDir)
12
+ fs.writeFileSync(path.join(tmpDir, 'demo.yml'), 'key: origin')
13
+ fs.writeFileSync(path.join(tmpDir, 'demo.zh-CN.yml'), 'key: zh-CN')
14
+ fs.writeFileSync(path.join(tmpDir, 'demo.en.json'), JSON.stringify({ key: 'en' }, null, 2))
15
+ fs.writeFileSync(path.join(tmpDir, 'demodemo.yml'), 'key: Unknown')
16
+ })
17
+
18
+ afterAll(() => {
19
+ fs.removeSync(tmpDir)
20
+ })
21
+
22
+ it('应该加载基础的 i18n 配置和所有特定语言的配置', async () => {
23
+ const config = await loadI18nConfig(`${tmpDir}/demo`)
24
+ expect(config).toEqual({
25
+ origin: { key: 'origin' },
26
+ 'zh-CN': { key: 'zh-CN' },
27
+ en: { key: 'en' }
28
+ })
29
+ })
30
+ })
@@ -0,0 +1,48 @@
1
+ import load from './load/load.js'
2
+ import { glob } from 'glob'
3
+ import path from 'path'
4
+
5
+ // Extracted utility functions
6
+ import { filterOneDot, filterDoubleDot, getRelativePath, joinPath } from '../utils/pathUtils.js'
7
+
8
+ export const cache = {}
9
+
10
+ export async function register(namespace, data) {
11
+ namespace = namespace.toLowerCase()
12
+ // namespace = namespace.toUpperCase()
13
+ cache[namespace] = cache[namespace] || {}
14
+
15
+ // 传入路径,则根据路径读取数据
16
+ if (typeof data === 'string') {
17
+ data = await load(data)
18
+ }
19
+
20
+ for (const key in data) {
21
+ cache[namespace][key] = cache[namespace][key] || {}
22
+ cache[namespace][key] = {
23
+ ...cache[namespace][key],
24
+ ...data[key]
25
+ }
26
+ }
27
+ }
28
+
29
+ export async function registerAll(directory) {
30
+ const absDirectory = path.resolve(directory)
31
+
32
+ const files = glob.sync(`${absDirectory}/**/*.*`)
33
+ const originFiles = filterOneDot(files)
34
+ const extFiles = filterDoubleDot(files)
35
+
36
+ // 处理扩展文件列表,去除扩展属性
37
+ const extFilesOrigin = extFiles.map(file => {
38
+ const paths = file.split('.')
39
+ return paths[0] + '.' + paths[2]
40
+ })
41
+
42
+ const allOriginFiles = originFiles.concat(extFilesOrigin)
43
+
44
+ for (const file of allOriginFiles) {
45
+ const ns = getRelativePath(absDirectory, file)
46
+ await register(ns, joinPath(process.cwd(), file))
47
+ }
48
+ }
@@ -0,0 +1,84 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from '@jest/globals'
2
+ import fs from 'fs-extra'
3
+ import path from 'path'
4
+ import { registerAll, cache, register } from './registry.js'
5
+ import tmp from '../../test-utils/tmp.js'
6
+
7
+ const tmp1Dir = await tmp('i18nRegistry1')
8
+ const tmp2Dir = await tmp('i18nRegistry2')
9
+ const tmp3Dir = await tmp('i18nRegistry3')
10
+
11
+ describe('注册I18n', () => {
12
+ beforeAll(() => {
13
+ fs.ensureDirSync(tmp1Dir)
14
+ fs.writeFileSync(path.join(tmp1Dir, 'demo.yml'), 'key: origin')
15
+ fs.writeFileSync(path.join(tmp1Dir, 'demo.en.yml'), 'key: en')
16
+ fs.writeFileSync(path.join(tmp1Dir, 'demo.zh-CN.yml'), 'key: zh-CN')
17
+ fs.writeFileSync(path.join(tmp1Dir, 'demo2.json'), '{"key": "origin"}')
18
+ fs.writeFileSync(path.join(tmp1Dir, 'demo2.en.json'), '{"key": "en"}')
19
+ fs.writeFileSync(path.join(tmp1Dir, 'demo2.zh-CN.json'), '{"key": "zh-CN"}')
20
+
21
+ fs.ensureDirSync(tmp2Dir)
22
+ fs.writeFileSync(path.join(tmp2Dir, 'demo3.yml'), 'key: origin')
23
+ fs.ensureDirSync(tmp3Dir)
24
+ fs.writeFileSync(path.join(tmp3Dir, 'demo3.zh-CN.yml'), 'key: zh-CN')
25
+ })
26
+
27
+ afterAll(() => {
28
+ fs.removeSync(tmp1Dir)
29
+ fs.removeSync(tmp2Dir)
30
+ })
31
+
32
+ it('通过路径,注册所有指定路径下的i18n配置文件', async () => {
33
+ await registerAll(tmp1Dir)
34
+ expect(cache.demo).toBeDefined()
35
+ expect(cache.demo.origin).toBeDefined()
36
+ expect(cache.demo.en).toBeDefined()
37
+ expect(cache.demo['zh-CN']).toBeDefined()
38
+ expect(cache.demo.origin.key).toBe('origin')
39
+ expect(cache.demo.en.key).toBe('en')
40
+ expect(cache.demo['zh-CN'].key).toBe('zh-CN')
41
+
42
+ expect(cache.demo2).toBeDefined()
43
+ expect(cache.demo2.origin).toBeDefined()
44
+ expect(cache.demo2.en).toBeDefined()
45
+ expect(cache.demo2['zh-CN']).toBeDefined()
46
+ expect(cache.demo2.origin.key).toBe('origin')
47
+ expect(cache.demo2.en.key).toBe('en')
48
+ expect(cache.demo2['zh-CN'].key).toBe('zh-CN')
49
+ })
50
+
51
+ it('通过路径,注册单个文件', async () => {
52
+ await register('demo3', path.join(tmp2Dir, 'demo3.yml'))
53
+ expect(cache.demo3).toBeDefined()
54
+ expect(cache.demo3.origin).toBeDefined()
55
+ expect(cache.demo3.origin.key).toBe('origin')
56
+ })
57
+
58
+ it('通过路径,注册单个文件,命名空间已存在', async () => {
59
+ await register('demo3', path.join(tmp2Dir, 'demo3.yml'))
60
+ expect(cache.demo3).toBeDefined()
61
+ expect(cache.demo3.origin).toBeDefined()
62
+ expect(cache.demo3.origin.key).toBe('origin')
63
+ expect(cache.demo3['zh-CN']).toBeUndefined()
64
+ await register('demo3', path.join(tmp3Dir, 'demo3.yml'))
65
+ expect(cache.demo3['zh-CN']).toBeDefined()
66
+ expect(cache.demo3['zh-CN'].key).toBe('zh-CN')
67
+ })
68
+
69
+ it('通过数据,注册单个命名空间', async () => {
70
+ await register('demo4', {
71
+ origin: {
72
+ key: 'origin'
73
+ },
74
+ 'zh-CN': {
75
+ key: 'zh-CN'
76
+ }
77
+ })
78
+ expect(cache.demo4).toBeDefined()
79
+ expect(cache.demo4.origin).toBeDefined()
80
+ expect(cache.demo4.origin.key).toBe('origin')
81
+ expect(cache.demo4['zh-CN']).toBeDefined()
82
+ expect(cache.demo4['zh-CN'].key).toBe('zh-CN')
83
+ })
84
+ })
@@ -0,0 +1,14 @@
1
+ const parseFile = fileString => {
2
+ const logs = fileString.split('\n')
3
+ return logs
4
+ .filter(o => o.trim() !== '')
5
+ .map(o => {
6
+ const log = JSON.parse(o)
7
+ log.message = decodeURIComponent(log.message)
8
+ const dataString = decodeURIComponent(log.data)
9
+ log.data = JSON.parse(dataString)
10
+ return log
11
+ })
12
+ }
13
+
14
+ export default parseFile
@@ -0,0 +1,28 @@
1
+ import { describe, it, expect } from '@jest/globals'
2
+ import parseFile from './parseFile.js'
3
+
4
+ describe('parseFile', () => {
5
+ it('应该正确解析日志文件字符串', () => {
6
+ const fileString = `{"message":"test%20message","data":"%7B%22key%22%3A%22value%22%7D"}\n{"message":"another%20message","data":"%7B%22anotherKey%22%3A%22anotherValue%22%7D"}`
7
+ const result = parseFile(fileString)
8
+ expect(result).toEqual([
9
+ { message: 'test message', data: { key: 'value' } },
10
+ { message: 'another message', data: { anotherKey: 'anotherValue' } }
11
+ ])
12
+ })
13
+
14
+ it('应该返回空数组对于空文件字符串', () => {
15
+ const fileString = ''
16
+ const result = parseFile(fileString)
17
+ expect(result).toEqual([])
18
+ })
19
+
20
+ it('应该忽略文件字符串中的空行', () => {
21
+ const fileString = `{"message":"test%20message","data":"%7B%22key%22%3A%22value%22%7D"}\n\n{"message":"another%20message","data":"%7B%22anotherKey%22%3A%22anotherValue%22%7D"}`
22
+ const result = parseFile(fileString)
23
+ expect(result).toEqual([
24
+ { message: 'test message', data: { key: 'value' } },
25
+ { message: 'another message', data: { anotherKey: 'anotherValue' } }
26
+ ])
27
+ })
28
+ })
@@ -0,0 +1,48 @@
1
+ import chalk from 'chalk'
2
+
3
+ const stringifyCMD = ({
4
+ level = 'info',
5
+ timestamp = Date.now(),
6
+ namespace = 'default',
7
+ id = ''
8
+ }) => {
9
+ const date = new Date(timestamp)
10
+ const formattedTimestamp = date.toISOString().replace('T', ' ').replace('Z', '')
11
+
12
+ let colorFn
13
+ switch (level) {
14
+ case 'trace':
15
+ colorFn = chalk.gray
16
+ break
17
+ case 'debug':
18
+ colorFn = chalk.green
19
+ break
20
+ case 'info':
21
+ colorFn = chalk.blue
22
+ break
23
+ case 'warn':
24
+ colorFn = chalk.yellow
25
+ break
26
+ case 'error':
27
+ colorFn = chalk.red
28
+ break
29
+ default:
30
+ colorFn = chalk.white
31
+ }
32
+
33
+ const prefixes = []
34
+ prefixes.push(formattedTimestamp)
35
+ prefixes.push(level.toUpperCase())
36
+ prefixes.push(namespace.toUpperCase())
37
+ if (id) {
38
+ prefixes.push(id)
39
+ }
40
+ const prefixString = prefixes
41
+ .map(prefix => {
42
+ return `[${prefix}]`
43
+ })
44
+ .join(' ')
45
+ return colorFn(`${prefixString} -`)
46
+ }
47
+
48
+ export default stringifyCMD
@@ -0,0 +1,37 @@
1
+ import { describe, it, expect } from '@jest/globals'
2
+ import stringify from './stringifyCMD.js'
3
+ import chalk from 'chalk'
4
+
5
+ const timestamp = 1609459200000 // 2021-01-01T00:00:00.000Z
6
+
7
+ describe('日志对象转命令行输出字符串', () => {
8
+ it('应该使用默认值格式化日志消息', () => {
9
+ const result = stringify({}).replace(/\.\d{3}/, '')
10
+ const date = new Date().toISOString().replace('T', ' ').replace('Z', '')
11
+ expect(result).toBe(chalk.blue(`[${date}] [INFO] [DEFAULT] -`.replace(/\.\d{3}/, '')))
12
+ })
13
+
14
+ it('应该使用自定义值格式化日志消息', () => {
15
+ const result = stringify({
16
+ level: 'error',
17
+ timestamp,
18
+ namespace: 'custom',
19
+ id: '12345'
20
+ })
21
+ expect(result).toBe(chalk.red('[2021-01-01 00:00:00.000] [ERROR] [CUSTOM] [12345] -'))
22
+ })
23
+
24
+ it('应该为每个日志级别使用正确的颜色', () => {
25
+ const levels = ['trace', 'debug', 'info', 'warn', 'error', 'unknown']
26
+ const colors = [chalk.gray, chalk.green, chalk.blue, chalk.yellow, chalk.red, chalk.white]
27
+ levels.forEach((level, color) => {
28
+ const result = stringify({
29
+ level,
30
+ timestamp,
31
+ id: '12345'
32
+ })
33
+ const date = new Date(timestamp).toISOString().replace('T', ' ').replace('Z', '')
34
+ expect(result).toBe(colors[color](`[${date}] [${level.toUpperCase()}] [DEFAULT] [12345] -`))
35
+ })
36
+ })
37
+ })
@@ -0,0 +1,24 @@
1
+ const stringifyFile = ({
2
+ level = 'info',
3
+ timestamp = Date.now(),
4
+ namespace = 'DEFAULT',
5
+ id = '',
6
+ message = '',
7
+ code = '',
8
+ data = {}
9
+ }) => {
10
+ // Encode message to Base64
11
+ const encodedMessage = encodeURIComponent(message)
12
+ const encodedData = encodeURIComponent(JSON.stringify(data))
13
+ return JSON.stringify({
14
+ level,
15
+ timestamp,
16
+ namespace,
17
+ id,
18
+ message: encodedMessage,
19
+ code,
20
+ data: encodedData
21
+ })
22
+ }
23
+
24
+ export default stringifyFile
@@ -0,0 +1,79 @@
1
+ import { describe, it, expect } from '@jest/globals'
2
+ import stringifyFile from './stringifyFile.js'
3
+ import parseFile from './parseFile.js'
4
+
5
+ describe('日志对象转文件输出字符串', () => {
6
+ it('应该使用默认值记录消息', () => {
7
+ let logString = ''
8
+
9
+ const message = '测试消息'
10
+ logString += stringifyFile({ message })
11
+
12
+ const log = parseFile(logString)[0]
13
+
14
+ expect(log.level).toBe('info')
15
+ expect(log.message).toBe(message)
16
+ expect(log.timestamp).toBeGreaterThan(0)
17
+ expect(log.namespace).toBe('DEFAULT')
18
+ expect(log.id).toBe('')
19
+ expect(log.code).toBe('')
20
+ expect(log.data).toEqual({})
21
+ })
22
+
23
+ it('应该记录多条消息', () => {
24
+ let logString = ''
25
+
26
+ const messages = ['测试消息 1', '测试消息 2', '测试消息 3']
27
+ messages.forEach(message => {
28
+ logString += stringifyFile({ message }) + '\n'
29
+ })
30
+
31
+ const logs = parseFile(logString)
32
+
33
+ logs.forEach((log, index) => {
34
+ expect(log.level).toBe('info')
35
+ expect(log.message).toBe(messages[index])
36
+ expect(log.timestamp).toBeGreaterThan(0)
37
+ expect(log.namespace).toBe('DEFAULT')
38
+ expect(log.id).toBe('')
39
+ expect(log.code).toBe('')
40
+ expect(log.data).toEqual({})
41
+ })
42
+ })
43
+
44
+ it('应该记录带有特殊字符的消息', () => {
45
+ let logString = ''
46
+
47
+ const message = '带有特殊字符的测试消息\n新行\t制表符'
48
+ logString += stringifyFile({ message })
49
+
50
+ const log = parseFile(logString)[0]
51
+
52
+ expect(log.level).toBe('info')
53
+ expect(log.message).toBe(message)
54
+ expect(log.timestamp).toBeGreaterThan(0)
55
+ expect(log.namespace).toBe('DEFAULT')
56
+ expect(log.id).toBe('')
57
+ expect(log.code).toBe('')
58
+ expect(log.data).toEqual({})
59
+ })
60
+
61
+ it('应该记录带有code和data的消息', () => {
62
+ let logString = ''
63
+
64
+ const message = '测试消息'
65
+ const code = 'testKey'
66
+ const data = { foo: 'bar' }
67
+ logString += stringifyFile({ message, code, data })
68
+
69
+ const log = parseFile(logString)[0]
70
+
71
+ expect(log.level).toBe('info')
72
+ expect(log.message).toBe(message)
73
+ expect(log.timestamp).toBeGreaterThan(0)
74
+ expect(log.namespace).toBe('DEFAULT')
75
+ expect(log.id).toBe('')
76
+ expect(log.code).toBe(code)
77
+ expect(log.data).toEqual(data)
78
+ })
79
+ })