@gravito/cosmos 3.1.0 → 3.2.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/MIGRATION.md ADDED
@@ -0,0 +1,331 @@
1
+ # Migration Guide: Cosmos v1.x → v2.0
2
+
3
+ ## Overview
4
+
5
+ Cosmos v2.0 introduces Edge Runtime support while maintaining full backward compatibility with v1.x. This guide helps you migrate to v2.0 and leverage new features.
6
+
7
+ ## Breaking Changes
8
+
9
+ **None!** v2.0 is fully backward compatible with v1.x.
10
+
11
+ All existing code will continue to work without modifications.
12
+
13
+ ## What's New in v2.0
14
+
15
+ ### Edge Runtime Support
16
+
17
+ Cosmos now runs in Edge Runtime environments:
18
+ - ✅ Cloudflare Workers
19
+ - ✅ Vercel Edge Functions
20
+ - ✅ Deno Deploy
21
+ - ✅ Node.js (existing support)
22
+
23
+ ### New Loaders
24
+
25
+ - **MemoryLoader**: Static translations for Edge environments
26
+ - **EdgeKVLoader**: Generic KV storage abstraction
27
+ - **CloudflareKVLoader**: Cloudflare Workers KV
28
+ - **VercelKVLoader**: Vercel KV
29
+
30
+ ### Runtime Detection
31
+
32
+ - `detectRuntime()`: Auto-detect execution environment
33
+ - `isNode()`, `isEdge()`: Helper functions
34
+
35
+ ## Migration Paths
36
+
37
+ ### For Node.js Projects
38
+
39
+ **No changes required!** Your existing code works as-is.
40
+
41
+ ```typescript
42
+ // ✅ This still works
43
+ import { OrbitCosmos } from '@gravito/cosmos'
44
+
45
+ const cosmos = new OrbitCosmos({
46
+ defaultLocale: 'zh-TW',
47
+ supportedLocales: ['zh-TW', 'en'],
48
+ lazyLoad: {
49
+ baseDir: './lang'
50
+ }
51
+ })
52
+ ```
53
+
54
+ ### For Edge Runtime Projects
55
+
56
+ #### Option 1: Static Translations (Recommended for <100KB)
57
+
58
+ ```typescript
59
+ // Before: Not possible
60
+ // After: Use MemoryLoader
61
+ import { OrbitCosmos, MemoryLoader } from '@gravito/cosmos/edge'
62
+ import en from './lang/en.json'
63
+ import zhTW from './lang/zh-TW.json'
64
+
65
+ const cosmos = new OrbitCosmos({
66
+ defaultLocale: 'zh-TW',
67
+ supportedLocales: ['zh-TW', 'en'],
68
+ loaders: [
69
+ new MemoryLoader({
70
+ translations: { en, 'zh-TW': zhTW }
71
+ })
72
+ ]
73
+ })
74
+ ```
75
+
76
+ #### Option 2: Remote API (Recommended for dynamic content)
77
+
78
+ ```typescript
79
+ import { OrbitCosmos, RemoteLoader } from '@gravito/cosmos/edge'
80
+
81
+ const cosmos = new OrbitCosmos({
82
+ defaultLocale: 'zh-TW',
83
+ supportedLocales: ['zh-TW', 'en'],
84
+ loaders: [
85
+ new RemoteLoader({
86
+ url: 'https://api.example.com/i18n/:locale',
87
+ etagCache: true,
88
+ retries: 3
89
+ })
90
+ ]
91
+ })
92
+ ```
93
+
94
+ #### Option 3: KV Storage (Recommended for Cloudflare/Vercel)
95
+
96
+ **Cloudflare Workers:**
97
+
98
+ ```typescript
99
+ import { OrbitCosmos, CloudflareKVLoader } from '@gravito/cosmos/edge'
100
+
101
+ export interface Env {
102
+ I18N_KV: KVNamespace
103
+ }
104
+
105
+ export default {
106
+ async fetch(request: Request, env: Env) {
107
+ const cosmos = new OrbitCosmos({
108
+ defaultLocale: 'zh-TW',
109
+ supportedLocales: ['zh-TW', 'en'],
110
+ loaders: [
111
+ new CloudflareKVLoader({
112
+ namespace: env.I18N_KV
113
+ })
114
+ ]
115
+ })
116
+
117
+ // ... use cosmos
118
+ }
119
+ }
120
+ ```
121
+
122
+ **Vercel Edge Functions:**
123
+
124
+ ```typescript
125
+ import { OrbitCosmos, VercelKVLoader } from '@gravito/cosmos/edge'
126
+ import { kv } from '@vercel/kv'
127
+
128
+ const cosmos = new OrbitCosmos({
129
+ defaultLocale: 'zh-TW',
130
+ supportedLocales: ['zh-TW', 'en'],
131
+ loaders: [
132
+ new VercelKVLoader({ kv })
133
+ ]
134
+ })
135
+ ```
136
+
137
+ ## Best Practices
138
+
139
+ ### 1. Use Fallback Chains
140
+
141
+ Combine loaders for reliability:
142
+
143
+ ```typescript
144
+ import { MemoryLoader, RemoteLoader, CloudflareKVLoader } from '@gravito/cosmos/edge'
145
+
146
+ const cosmos = new OrbitCosmos({
147
+ loaders: [
148
+ new CloudflareKVLoader({ namespace: env.I18N_KV })
149
+ .fallback(new RemoteLoader({ url: env.I18N_API }))
150
+ .fallback(new MemoryLoader({ translations: fallbackData }))
151
+ ]
152
+ })
153
+ ```
154
+
155
+ ### 2. Choose the Right Loader
156
+
157
+ | Scenario | Recommended Loader | Reason |
158
+ |----------|-------------------|--------|
159
+ | Small static translations (<100KB) | MemoryLoader | Fastest, no network |
160
+ | Dynamic CMS translations | RemoteLoader | Real-time updates |
161
+ | Cloudflare Workers | CloudflareKVLoader | Edge-optimized |
162
+ | Vercel Edge | VercelKVLoader | Edge-optimized |
163
+ | Node.js server | FileSystemLoader | Native fs access |
164
+
165
+ ### 3. Optimize Bundle Size
166
+
167
+ Edge environments have size limits:
168
+
169
+ ```typescript
170
+ // ✅ Good: Import only what you need
171
+ import { OrbitCosmos, MemoryLoader } from '@gravito/cosmos/edge'
172
+
173
+ // ❌ Avoid: Importing Node.js-only features in Edge
174
+ import { FileSystemLoader } from '@gravito/cosmos/node'
175
+ ```
176
+
177
+ ## Deprecated Features
178
+
179
+ ### lazyLoad.loader (Still works, but deprecated)
180
+
181
+ ```typescript
182
+ // ⚠️ Deprecated
183
+ {
184
+ lazyLoad: {
185
+ baseDir: './lang',
186
+ loader: customLoaderFn
187
+ }
188
+ }
189
+
190
+ // ✅ Recommended
191
+ {
192
+ loaders: [
193
+ new FileSystemLoader({ baseDir: './lang' })
194
+ ]
195
+ }
196
+ ```
197
+
198
+ ## Troubleshooting
199
+
200
+ ### Error: "FileSystemLoader requires Node.js"
201
+
202
+ **Cause**: Using FileSystemLoader in Edge Runtime
203
+
204
+ **Solution**: Use MemoryLoader, RemoteLoader, or EdgeKVLoader
205
+
206
+ ```typescript
207
+ // ❌ Wrong
208
+ import { FileSystemLoader } from '@gravito/cosmos/edge'
209
+
210
+ // ✅ Correct
211
+ import { MemoryLoader } from '@gravito/cosmos/edge'
212
+ ```
213
+
214
+ ### Warning: "HMR is not supported in Edge Runtime"
215
+
216
+ **Cause**: HMRWatcher is Node.js-only
217
+
218
+ **Solution**: Use RemoteLoader with ETag caching for dynamic updates
219
+
220
+ ```typescript
221
+ // Edge Runtime alternative to HMR
222
+ new RemoteLoader({
223
+ url: 'https://api.example.com/i18n/:locale',
224
+ etagCache: true // Automatically refetches when content changes
225
+ })
226
+ ```
227
+
228
+ ### Translations not loading
229
+
230
+ **Debug steps:**
231
+
232
+ 1. Check loader configuration
233
+ 2. Verify translations are uploaded (for KV loaders)
234
+ 3. Check network requests (for RemoteLoader)
235
+ 4. Enable verbose logging
236
+
237
+ ```typescript
238
+ const loader = new RemoteLoader({
239
+ url: 'https://api.example.com/i18n/:locale',
240
+ // Add console.log in load method for debugging
241
+ })
242
+
243
+ console.log('Testing load:', await loader.load('en'))
244
+ ```
245
+
246
+ ## Examples
247
+
248
+ ### Cloudflare Workers Complete Example
249
+
250
+ ```typescript
251
+ import { OrbitCosmos, CloudflareKVLoader, MemoryLoader } from '@gravito/cosmos/edge'
252
+
253
+ export interface Env {
254
+ I18N_KV: KVNamespace
255
+ }
256
+
257
+ // Fallback translations
258
+ const fallback = {
259
+ en: { error: 'An error occurred' },
260
+ 'zh-TW': { error: '發生錯誤' }
261
+ }
262
+
263
+ export default {
264
+ async fetch(request: Request, env: Env) {
265
+ const cosmos = new OrbitCosmos({
266
+ defaultLocale: 'zh-TW',
267
+ supportedLocales: ['zh-TW', 'en'],
268
+ loaders: [
269
+ new CloudflareKVLoader({ namespace: env.I18N_KV })
270
+ .fallback(new MemoryLoader({ translations: fallback }))
271
+ ]
272
+ })
273
+
274
+ const i18n = cosmos.clone('zh-TW')
275
+ await i18n.ensureLocale('zh-TW')
276
+
277
+ return new Response(i18n.t('welcome'))
278
+ }
279
+ }
280
+ ```
281
+
282
+ ### Vercel Edge Functions Complete Example
283
+
284
+ ```typescript
285
+ import { OrbitCosmos, VercelKVLoader } from '@gravito/cosmos/edge'
286
+ import { kv } from '@vercel/kv'
287
+
288
+ const cosmos = new OrbitCosmos({
289
+ defaultLocale: 'zh-TW',
290
+ supportedLocales: ['zh-TW', 'en'],
291
+ loaders: [
292
+ new VercelKVLoader({ kv, prefix: 'i18n' })
293
+ ]
294
+ })
295
+
296
+ export async function GET(request: Request) {
297
+ const url = new URL(request.url)
298
+ const locale = url.searchParams.get('lang') || 'zh-TW'
299
+
300
+ const i18n = cosmos.clone(locale)
301
+ await i18n.ensureLocale(locale)
302
+
303
+ return new Response(JSON.stringify({
304
+ message: i18n.t('welcome'),
305
+ locale: i18n.getLocale()
306
+ }))
307
+ }
308
+ ```
309
+
310
+ ## FAQ
311
+
312
+ **Q: Do I need to update my code for v2.0?**
313
+ A: No, v2.0 is fully backward compatible.
314
+
315
+ **Q: Can I use FileSystemLoader in Edge Runtime?**
316
+ A: No, use MemoryLoader, RemoteLoader, or EdgeKVLoader instead.
317
+
318
+ **Q: How do I upload translations to KV storage?**
319
+ A: See the CloudflareKVLoader and VercelKVLoader documentation for upload scripts.
320
+
321
+ **Q: What's the performance difference between loaders?**
322
+ A: MemoryLoader (fastest) > KV Loaders (fast) > RemoteLoader (depends on network)
323
+
324
+ **Q: Can I mix Node.js and Edge loaders?**
325
+ A: In Node.js, yes. In Edge, only Edge-compatible loaders work.
326
+
327
+ ## Support
328
+
329
+ - Documentation: [README.md](./README.md)
330
+ - Architecture: [docs/architecture/cosmos.md](../../docs/architecture/cosmos.md)
331
+ - Issues: [GitHub Issues](https://github.com/gravito-framework/gravito/issues)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravito/cosmos",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Internationalization orbit for Gravito framework",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -12,14 +12,26 @@
12
12
  "types": "./dist/index.d.ts",
13
13
  "import": "./dist/index.js",
14
14
  "require": "./dist/index.cjs"
15
+ },
16
+ "./edge": {
17
+ "types": "./dist/index.edge.d.ts",
18
+ "import": "./dist/index.edge.js",
19
+ "require": "./dist/index.edge.cjs"
20
+ },
21
+ "./node": {
22
+ "types": "./dist/index.node.d.ts",
23
+ "import": "./dist/index.node.js",
24
+ "require": "./dist/index.node.cjs"
15
25
  }
16
26
  },
17
27
  "scripts": {
18
28
  "build": "bun run build.ts",
19
- "test": "bun test",
20
- "test:coverage": "bun test --coverage --coverage-threshold=80",
21
- "test:ci": "bun test --coverage --coverage-threshold=80",
22
- "typecheck": "bun tsc -p tsconfig.json --noEmit --skipLibCheck"
29
+ "test": "bun test --timeout=10000",
30
+ "test:coverage": "bun test --timeout=10000 --coverage --coverage-reporter=lcov --coverage-dir coverage && bun run --bun scripts/check-coverage.ts",
31
+ "test:ci": "bun test --timeout=10000 --coverage --coverage-reporter=lcov --coverage-dir coverage && bun run --bun scripts/check-coverage.ts",
32
+ "typecheck": "bun tsc -p tsconfig.json --noEmit --skipLibCheck",
33
+ "test:unit": "bun test tests/ --timeout=10000",
34
+ "test:integration": "test $(find tests -name '*.integration.test.ts' 2>/dev/null | wc -l) -gt 0 && find tests -name '*.integration.test.ts' -print0 | xargs -0 bun test --timeout=10000 || echo 'No integration tests found'"
23
35
  },
24
36
  "keywords": [
25
37
  "gravito",
@@ -47,5 +59,8 @@
47
59
  "type": "git",
48
60
  "url": "git+https://github.com/gravito-framework/gravito.git",
49
61
  "directory": "packages/cosmos"
62
+ },
63
+ "dependencies": {
64
+ "lru-cache": "^11.2.5"
50
65
  }
51
66
  }
@@ -0,0 +1,64 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+
4
+ const lcovPath = process.argv[2] ?? 'coverage/lcov.info'
5
+ const threshold = Number.parseFloat(process.env.COVERAGE_THRESHOLD ?? '80')
6
+
7
+ const root = resolve(process.cwd())
8
+ const srcRoot = `${resolve(root, 'src')}/`
9
+
10
+ // 檢查 lcov.info 是否存在
11
+ if (!existsSync(lcovPath)) {
12
+ console.error(`Coverage file not found: ${lcovPath}`)
13
+ process.exit(1)
14
+ }
15
+
16
+ let content: string
17
+ try {
18
+ content = readFileSync(lcovPath, 'utf-8')
19
+ } catch (error) {
20
+ console.error(`Failed to read coverage file: ${error}`)
21
+ process.exit(1)
22
+ }
23
+
24
+ const lines = content.split('\n')
25
+
26
+ let currentFile: string | null = null
27
+ let total = 0
28
+ let hit = 0
29
+
30
+ for (const line of lines) {
31
+ if (line.startsWith('SF:')) {
32
+ const filePath = line.slice(3).trim()
33
+ const abs = resolve(root, filePath)
34
+ currentFile = abs.startsWith(srcRoot) ? abs : null
35
+ continue
36
+ }
37
+
38
+ if (!currentFile) {
39
+ continue
40
+ }
41
+
42
+ if (line.startsWith('DA:')) {
43
+ const parts = line.slice(3).split(',')
44
+ if (parts.length >= 2) {
45
+ total += 1
46
+ const count = Number.parseInt(parts[1] ?? '0', 10)
47
+ if (count > 0) {
48
+ hit += 1
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ const percent = total === 0 ? 0 : (hit / total) * 100
55
+ const rounded = Math.round(percent * 100) / 100
56
+
57
+ if (rounded < threshold) {
58
+ console.error(
59
+ `cosmos coverage ${rounded}% is below threshold ${threshold}%. Covered lines: ${hit}/${total}.`
60
+ )
61
+ process.exit(1)
62
+ }
63
+
64
+ console.log(`cosmos coverage ${rounded}% (${hit}/${total}) meets threshold ${threshold}%.`)