@gravito/cosmos 3.2.2 → 4.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/package.json +3 -3
- package/src/I18nService.ts +5 -1
- package/src/errors/CosmosError.ts +20 -0
- package/src/errors/codes.ts +30 -0
- package/src/errors/index.ts +2 -0
- package/src/index.ts +3 -0
- package/src/loader.ts +7 -4
- package/src/loaders/EdgeKVLoader.ts +8 -2
- package/src/loaders/FileSystemLoader.ts +12 -8
- package/src/loaders/Json5Loader.ts +12 -8
- package/src/loaders/RemoteLoader.ts +5 -1
- package/tests/contract/cosmos-errors.contract.test.ts +73 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gravito/cosmos",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Internationalization orbit for Gravito framework",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"author": "Carl Lee <carllee0520@gmail.com>",
|
|
44
44
|
"license": "MIT",
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@gravito/core": "^
|
|
47
|
-
"@gravito/photon": "^
|
|
46
|
+
"@gravito/core": "^3.0.0",
|
|
47
|
+
"@gravito/photon": "^2.0.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@gravito/core": "workspace:*",
|
package/src/I18nService.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
import type { GravitoMiddleware } from '@gravito/core'
|
|
8
8
|
import { LRUCache } from 'lru-cache'
|
|
9
9
|
import { type HMRConfig, HMRWatcher } from './HMRWatcher'
|
|
10
|
+
import { CosmosError } from './errors/CosmosError'
|
|
11
|
+
import { CosmosErrorCodes } from './errors/codes'
|
|
10
12
|
import { loadLocale } from './loader'
|
|
11
13
|
import type { TranslationLoader } from './loaders/TranslationLoader'
|
|
12
14
|
|
|
@@ -816,7 +818,9 @@ export class I18nManager<Schema = TranslationMap> implements I18nService<Schema>
|
|
|
816
818
|
case 'empty':
|
|
817
819
|
return ''
|
|
818
820
|
case 'throw':
|
|
819
|
-
throw new
|
|
821
|
+
throw new CosmosError(500, CosmosErrorCodes.MISSING_TRANSLATION, {
|
|
822
|
+
message: `Missing translation: ${key}`,
|
|
823
|
+
})
|
|
820
824
|
default:
|
|
821
825
|
return key
|
|
822
826
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { SystemException, type ExceptionOptions } from '@gravito/core'
|
|
2
|
+
import type { CosmosErrorCode } from './codes'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Base error class for @gravito/cosmos i18n operations.
|
|
6
|
+
* Extends SystemException as cosmos is a general system utility (not a domain/auth module).
|
|
7
|
+
*
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export class CosmosError extends SystemException {
|
|
11
|
+
constructor(
|
|
12
|
+
status: number,
|
|
13
|
+
code: CosmosErrorCode,
|
|
14
|
+
options: ExceptionOptions = {}
|
|
15
|
+
) {
|
|
16
|
+
super(status, code, options)
|
|
17
|
+
this.name = 'CosmosError'
|
|
18
|
+
Object.setPrototypeOf(this, new.target.prototype)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured error codes for @gravito/cosmos i18n operations.
|
|
3
|
+
* Follows the dot-separated namespace convention used across Gravito.
|
|
4
|
+
*
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export const CosmosErrorCodes = {
|
|
8
|
+
// Translation errors
|
|
9
|
+
MISSING_TRANSLATION: 'cosmos.missing_translation',
|
|
10
|
+
|
|
11
|
+
// Loader errors
|
|
12
|
+
LOADER_FAILED: 'cosmos.loader_failed',
|
|
13
|
+
UNSUPPORTED_FORMAT: 'cosmos.unsupported_format',
|
|
14
|
+
|
|
15
|
+
// Filesystem loader errors
|
|
16
|
+
FILE_NOT_FOUND: 'cosmos.file_not_found',
|
|
17
|
+
FILE_READ_FAILED: 'cosmos.file_read_failed',
|
|
18
|
+
|
|
19
|
+
// Remote loader errors
|
|
20
|
+
HTTP_ERROR: 'cosmos.http_error',
|
|
21
|
+
|
|
22
|
+
// Runtime errors
|
|
23
|
+
EDGE_RUNTIME_UNSUPPORTED: 'cosmos.edge_runtime_unsupported',
|
|
24
|
+
|
|
25
|
+
// KV storage errors
|
|
26
|
+
KV_PUT_UNSUPPORTED: 'cosmos.kv_put_unsupported',
|
|
27
|
+
KV_DELETE_UNSUPPORTED: 'cosmos.kv_delete_unsupported',
|
|
28
|
+
} as const
|
|
29
|
+
|
|
30
|
+
export type CosmosErrorCode = (typeof CosmosErrorCodes)[keyof typeof CosmosErrorCodes]
|
package/src/index.ts
CHANGED
package/src/loader.ts
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
|
|
22
22
|
import { readdir, readFile } from 'node:fs/promises'
|
|
23
23
|
import { join, parse } from 'node:path'
|
|
24
|
+
import { CosmosError } from './errors/CosmosError'
|
|
25
|
+
import { CosmosErrorCodes } from './errors/codes'
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
28
|
* 偵測 Bun 運行時是否原生支援 JSON5 解析
|
|
@@ -56,10 +58,11 @@ async function parseJson5(content: string): Promise<unknown> {
|
|
|
56
58
|
const json5 = json5Module.default ?? json5Module
|
|
57
59
|
return (json5 as { parse: (text: string) => unknown }).parse(content)
|
|
58
60
|
} catch {
|
|
59
|
-
throw new
|
|
60
|
-
|
|
61
|
-
'
|
|
62
|
-
|
|
61
|
+
throw new CosmosError(500, CosmosErrorCodes.UNSUPPORTED_FORMAT, {
|
|
62
|
+
message:
|
|
63
|
+
'[Cosmos] JSON5 parsing requires either Bun v1.2+ or the `json5` npm package. ' +
|
|
64
|
+
'Please install it: bun add json5',
|
|
65
|
+
})
|
|
63
66
|
}
|
|
64
67
|
}
|
|
65
68
|
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { TranslationMap } from '../I18nService'
|
|
8
|
+
import { CosmosError } from '../errors/CosmosError'
|
|
9
|
+
import { CosmosErrorCodes } from '../errors/codes'
|
|
8
10
|
import type { LoaderConfig, TranslationLoader, TranslationLoaderChain } from './TranslationLoader'
|
|
9
11
|
|
|
10
12
|
/**
|
|
@@ -219,7 +221,9 @@ export class EdgeKVLoader implements TranslationLoaderChain {
|
|
|
219
221
|
*/
|
|
220
222
|
async put(locale: string, translations: TranslationMap): Promise<void> {
|
|
221
223
|
if (!this.storage.put) {
|
|
222
|
-
throw new
|
|
224
|
+
throw new CosmosError(500, CosmosErrorCodes.KV_PUT_UNSUPPORTED, {
|
|
225
|
+
message: '[EdgeKVLoader] Storage does not support put operation',
|
|
226
|
+
})
|
|
223
227
|
}
|
|
224
228
|
|
|
225
229
|
const key = this.buildKey(locale)
|
|
@@ -239,7 +243,9 @@ export class EdgeKVLoader implements TranslationLoaderChain {
|
|
|
239
243
|
*/
|
|
240
244
|
async delete(locale: string): Promise<void> {
|
|
241
245
|
if (!this.storage.delete) {
|
|
242
|
-
throw new
|
|
246
|
+
throw new CosmosError(500, CosmosErrorCodes.KV_DELETE_UNSUPPORTED, {
|
|
247
|
+
message: '[EdgeKVLoader] Storage does not support delete operation',
|
|
248
|
+
})
|
|
243
249
|
}
|
|
244
250
|
|
|
245
251
|
const key = this.buildKey(locale)
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
import { readFile } from 'node:fs/promises'
|
|
20
20
|
import { join } from 'node:path'
|
|
21
21
|
import type { TranslationMap } from '../I18nService'
|
|
22
|
+
import { CosmosError } from '../errors/CosmosError'
|
|
23
|
+
import { CosmosErrorCodes } from '../errors/codes'
|
|
22
24
|
import { detectRuntime } from '../runtime/detector'
|
|
23
25
|
import type { LoaderConfig, TranslationLoader, TranslationLoaderChain } from './TranslationLoader'
|
|
24
26
|
|
|
@@ -96,10 +98,11 @@ export class FileSystemLoader implements TranslationLoaderChain {
|
|
|
96
98
|
// 運行時檢查
|
|
97
99
|
const runtime = detectRuntime()
|
|
98
100
|
if (runtime === 'edge') {
|
|
99
|
-
throw new
|
|
100
|
-
|
|
101
|
-
'
|
|
102
|
-
|
|
101
|
+
throw new CosmosError(500, CosmosErrorCodes.EDGE_RUNTIME_UNSUPPORTED, {
|
|
102
|
+
message:
|
|
103
|
+
'[FileSystemLoader] This loader requires Node.js and cannot run in Edge Runtime. ' +
|
|
104
|
+
'Use MemoryLoader, RemoteLoader, or EdgeKVLoader instead.',
|
|
105
|
+
})
|
|
103
106
|
}
|
|
104
107
|
|
|
105
108
|
this.name = config.name || 'FileSystemLoader'
|
|
@@ -161,10 +164,11 @@ export class FileSystemLoader implements TranslationLoaderChain {
|
|
|
161
164
|
const json5 = json5Module.default ?? json5Module
|
|
162
165
|
return (json5 as { parse: (text: string) => unknown }).parse(content) as TranslationMap
|
|
163
166
|
} catch {
|
|
164
|
-
throw new
|
|
165
|
-
|
|
166
|
-
'
|
|
167
|
-
|
|
167
|
+
throw new CosmosError(500, CosmosErrorCodes.UNSUPPORTED_FORMAT, {
|
|
168
|
+
message:
|
|
169
|
+
'[FileSystemLoader] JSON5 parsing requires either Bun v1.2+ (native support) ' +
|
|
170
|
+
'or the `json5` npm package. Please install it: bun add json5',
|
|
171
|
+
})
|
|
168
172
|
}
|
|
169
173
|
}
|
|
170
174
|
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
import { readFile } from 'node:fs/promises'
|
|
18
18
|
import { join } from 'node:path'
|
|
19
19
|
import type { TranslationMap } from '../I18nService'
|
|
20
|
+
import { CosmosError } from '../errors/CosmosError'
|
|
21
|
+
import { CosmosErrorCodes } from '../errors/codes'
|
|
20
22
|
import { detectRuntime } from '../runtime/detector'
|
|
21
23
|
import type { LoaderConfig, TranslationLoader, TranslationLoaderChain } from './TranslationLoader'
|
|
22
24
|
|
|
@@ -95,10 +97,11 @@ async function parseJson5Content(content: string): Promise<unknown> {
|
|
|
95
97
|
const json5 = json5Module.default ?? json5Module
|
|
96
98
|
return (json5 as { parse: (text: string) => unknown }).parse(content)
|
|
97
99
|
} catch {
|
|
98
|
-
throw new
|
|
99
|
-
|
|
100
|
-
'
|
|
101
|
-
|
|
100
|
+
throw new CosmosError(500, CosmosErrorCodes.UNSUPPORTED_FORMAT, {
|
|
101
|
+
message:
|
|
102
|
+
'[Json5Loader] JSON5 parsing requires either Bun v1.2+ (native support) ' +
|
|
103
|
+
'or the `json5` npm package. Please install it: bun add json5',
|
|
104
|
+
})
|
|
102
105
|
}
|
|
103
106
|
}
|
|
104
107
|
|
|
@@ -145,10 +148,11 @@ export class Json5Loader implements TranslationLoaderChain {
|
|
|
145
148
|
// 運行時檢查
|
|
146
149
|
const runtime = detectRuntime()
|
|
147
150
|
if (runtime === 'edge') {
|
|
148
|
-
throw new
|
|
149
|
-
|
|
150
|
-
'
|
|
151
|
-
|
|
151
|
+
throw new CosmosError(500, CosmosErrorCodes.EDGE_RUNTIME_UNSUPPORTED, {
|
|
152
|
+
message:
|
|
153
|
+
'[Json5Loader] This loader requires Node.js and cannot run in Edge Runtime. ' +
|
|
154
|
+
'Use MemoryLoader, RemoteLoader, or EdgeKVLoader instead.',
|
|
155
|
+
})
|
|
152
156
|
}
|
|
153
157
|
|
|
154
158
|
this.name = config.name ?? 'Json5Loader'
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { TranslationMap } from '../I18nService'
|
|
8
|
+
import { CosmosError } from '../errors/CosmosError'
|
|
9
|
+
import { CosmosErrorCodes } from '../errors/codes'
|
|
8
10
|
import type { LoaderConfig, TranslationLoader, TranslationLoaderChain } from './TranslationLoader'
|
|
9
11
|
|
|
10
12
|
/**
|
|
@@ -186,7 +188,9 @@ export class RemoteLoader implements TranslationLoaderChain {
|
|
|
186
188
|
|
|
187
189
|
// 處理錯誤狀態碼
|
|
188
190
|
if (!response.ok) {
|
|
189
|
-
throw new
|
|
191
|
+
throw new CosmosError(response.status, CosmosErrorCodes.HTTP_ERROR, {
|
|
192
|
+
message: `HTTP error! status: ${response.status}`,
|
|
193
|
+
})
|
|
190
194
|
}
|
|
191
195
|
|
|
192
196
|
const data = (await response.json()) as TranslationMap
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { GravitoException, SystemException } from '@gravito/core'
|
|
3
|
+
import { CosmosError } from '../../src/errors/CosmosError'
|
|
4
|
+
import { CosmosErrorCodes } from '../../src/errors/codes'
|
|
5
|
+
|
|
6
|
+
describe('CosmosError contract', () => {
|
|
7
|
+
it('satisfies GravitoException contract', () => {
|
|
8
|
+
const err = new CosmosError(500, CosmosErrorCodes.MISSING_TRANSLATION, {
|
|
9
|
+
message: 'Missing translation: common.welcome',
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
expect(err).toBeInstanceOf(Error)
|
|
13
|
+
expect(err).toBeInstanceOf(GravitoException)
|
|
14
|
+
expect(err).toBeInstanceOf(SystemException)
|
|
15
|
+
expect(err).toBeInstanceOf(CosmosError)
|
|
16
|
+
|
|
17
|
+
expect(err.code).toBe('cosmos.missing_translation')
|
|
18
|
+
expect(err.status).toBe(500)
|
|
19
|
+
expect(typeof err.code).toBe('string')
|
|
20
|
+
expect(typeof err.status).toBe('number')
|
|
21
|
+
expect(err.name).toBe('CosmosError')
|
|
22
|
+
expect(err.name).not.toBe('Error')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('extends SystemException (not DomainException or AuthException)', () => {
|
|
26
|
+
const err = new CosmosError(500, CosmosErrorCodes.LOADER_FAILED)
|
|
27
|
+
expect(err).toBeInstanceOf(SystemException)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('preserves message', () => {
|
|
31
|
+
const err = new CosmosError(500, CosmosErrorCodes.MISSING_TRANSLATION, {
|
|
32
|
+
message: 'Missing translation: test.key',
|
|
33
|
+
})
|
|
34
|
+
expect(err.message).toBe('Missing translation: test.key')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('preserves cause', () => {
|
|
38
|
+
const cause = new Error('original error')
|
|
39
|
+
const err = new CosmosError(500, CosmosErrorCodes.LOADER_FAILED, { cause })
|
|
40
|
+
expect(err.cause).toBe(cause)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('instanceof chain holds for all error codes', () => {
|
|
44
|
+
const codes = Object.values(CosmosErrorCodes)
|
|
45
|
+
for (const code of codes) {
|
|
46
|
+
const err = new CosmosError(500, code)
|
|
47
|
+
expect(err).toBeInstanceOf(CosmosError)
|
|
48
|
+
expect(err).toBeInstanceOf(SystemException)
|
|
49
|
+
expect(err).toBeInstanceOf(GravitoException)
|
|
50
|
+
expect(err).toBeInstanceOf(Error)
|
|
51
|
+
expect(err.code).toBe(code)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('all error codes are cosmos.* namespaced', () => {
|
|
56
|
+
const codes = Object.values(CosmosErrorCodes)
|
|
57
|
+
for (const code of codes) {
|
|
58
|
+
expect(code).toMatch(/^cosmos\./)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('CosmosErrorCodes covers expected domains', () => {
|
|
63
|
+
expect(CosmosErrorCodes.MISSING_TRANSLATION).toBe('cosmos.missing_translation')
|
|
64
|
+
expect(CosmosErrorCodes.LOADER_FAILED).toBe('cosmos.loader_failed')
|
|
65
|
+
expect(CosmosErrorCodes.UNSUPPORTED_FORMAT).toBe('cosmos.unsupported_format')
|
|
66
|
+
expect(CosmosErrorCodes.FILE_NOT_FOUND).toBe('cosmos.file_not_found')
|
|
67
|
+
expect(CosmosErrorCodes.FILE_READ_FAILED).toBe('cosmos.file_read_failed')
|
|
68
|
+
expect(CosmosErrorCodes.HTTP_ERROR).toBe('cosmos.http_error')
|
|
69
|
+
expect(CosmosErrorCodes.EDGE_RUNTIME_UNSUPPORTED).toBe('cosmos.edge_runtime_unsupported')
|
|
70
|
+
expect(CosmosErrorCodes.KV_PUT_UNSUPPORTED).toBe('cosmos.kv_put_unsupported')
|
|
71
|
+
expect(CosmosErrorCodes.KV_DELETE_UNSUPPORTED).toBe('cosmos.kv_delete_unsupported')
|
|
72
|
+
})
|
|
73
|
+
})
|