@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 +331 -0
- package/package.json +20 -5
- package/scripts/check-coverage.ts +64 -0
- package/src/HMRWatcher.ts +305 -0
- package/src/I18nService.ts +149 -9
- package/src/index.edge.ts +35 -0
- package/src/index.node.ts +20 -0
- package/src/index.ts +14 -0
- package/src/loader.ts +31 -14
- package/src/loaders/ChainedLoader.ts +117 -0
- package/src/loaders/CloudflareKVLoader.ts +194 -0
- package/src/loaders/EdgeKVLoader.ts +248 -0
- package/src/loaders/FileSystemLoader.ts +125 -0
- package/src/loaders/MemoryLoader.ts +161 -0
- package/src/loaders/RemoteLoader.ts +235 -0
- package/src/loaders/TranslationLoader.ts +98 -0
- package/src/loaders/VercelKVLoader.ts +192 -0
- package/src/runtime/detector.ts +97 -0
- package/src/runtime/path-utils.ts +169 -0
- package/tests/unit/detector.test.ts +1 -6
- package/tests/unit/edge-kv-loader.test.ts +202 -0
- package/tests/unit/hmr.test.ts +255 -0
- package/tests/unit/loader.test.ts +57 -29
- package/tests/unit/loaders.test.ts +332 -0
- package/tests/unit/memory-loader.test.ts +130 -0
- package/tests/unit/path-utils.test.ts +135 -0
- package/tests/unit/runtime-detector.test.ts +86 -0
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.
|
|
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-
|
|
21
|
-
"test:ci": "bun test --coverage --coverage-
|
|
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}%.`)
|