@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravito/cosmos",
3
- "version": "3.2.2",
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": "^2.0.0",
47
- "@gravito/photon": "^1.1.0"
46
+ "@gravito/core": "^3.0.0",
47
+ "@gravito/photon": "^2.0.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@gravito/core": "workspace:*",
@@ -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 Error(`Missing translation: ${key}`)
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]
@@ -0,0 +1,2 @@
1
+ export { CosmosError } from './CosmosError'
2
+ export { CosmosErrorCodes, type CosmosErrorCode } from './codes'
package/src/index.ts CHANGED
@@ -94,3 +94,6 @@ export * from './loaders/VercelKVLoader'
94
94
  // Runtime utilities
95
95
  export * from './runtime/detector'
96
96
  export * from './runtime/path-utils'
97
+
98
+ // Errors
99
+ export * from './errors'
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 Error(
60
- '[Cosmos] JSON5 parsing requires either Bun v1.2+ or the `json5` npm package. ' +
61
- 'Please install it: bun add json5'
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 Error('[EdgeKVLoader] Storage does not support put operation')
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 Error('[EdgeKVLoader] Storage does not support delete operation')
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 Error(
100
- '[FileSystemLoader] This loader requires Node.js and cannot run in Edge Runtime. ' +
101
- 'Use MemoryLoader, RemoteLoader, or EdgeKVLoader instead.'
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 Error(
165
- '[FileSystemLoader] JSON5 parsing requires either Bun v1.2+ (native support) ' +
166
- 'or the `json5` npm package. Please install it: bun add json5'
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 Error(
99
- '[Json5Loader] JSON5 parsing requires either Bun v1.2+ (native support) ' +
100
- 'or the `json5` npm package. Please install it: bun add json5'
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 Error(
149
- '[Json5Loader] This loader requires Node.js and cannot run in Edge Runtime. ' +
150
- 'Use MemoryLoader, RemoteLoader, or EdgeKVLoader instead.'
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 Error(`HTTP error! status: ${response.status}`)
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
+ })