melaka 0.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/CONTRIBUTING.md +347 -0
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/docs/AI_PROVIDERS.md +343 -0
- package/docs/ARCHITECTURE.md +512 -0
- package/docs/CLI.md +438 -0
- package/docs/CONFIGURATION.md +453 -0
- package/docs/INTEGRATION.md +477 -0
- package/docs/ROADMAP.md +248 -0
- package/package.json +46 -0
- package/packages/ai/README.md +43 -0
- package/packages/ai/package.json +42 -0
- package/packages/ai/src/facade.ts +120 -0
- package/packages/ai/src/index.ts +34 -0
- package/packages/ai/src/prompt.ts +117 -0
- package/packages/ai/src/providers/gemini.ts +185 -0
- package/packages/ai/src/providers/index.ts +9 -0
- package/packages/ai/src/types.ts +134 -0
- package/packages/ai/tsconfig.json +19 -0
- package/packages/cli/README.md +70 -0
- package/packages/cli/package.json +44 -0
- package/packages/cli/src/cli.ts +30 -0
- package/packages/cli/src/commands/deploy.ts +115 -0
- package/packages/cli/src/commands/index.ts +9 -0
- package/packages/cli/src/commands/init.ts +107 -0
- package/packages/cli/src/commands/status.ts +73 -0
- package/packages/cli/src/commands/translate.ts +92 -0
- package/packages/cli/src/commands/validate.ts +69 -0
- package/packages/cli/tsconfig.json +19 -0
- package/packages/core/README.md +46 -0
- package/packages/core/package.json +50 -0
- package/packages/core/src/config.ts +241 -0
- package/packages/core/src/index.ts +111 -0
- package/packages/core/src/schema-generator.ts +263 -0
- package/packages/core/src/schemas.ts +126 -0
- package/packages/core/src/types.ts +481 -0
- package/packages/core/src/utils.ts +343 -0
- package/packages/core/tsconfig.json +19 -0
- package/packages/firestore/README.md +60 -0
- package/packages/firestore/package.json +48 -0
- package/packages/firestore/src/generator.ts +270 -0
- package/packages/firestore/src/i18n.ts +262 -0
- package/packages/firestore/src/index.ts +54 -0
- package/packages/firestore/src/processor.ts +245 -0
- package/packages/firestore/src/queue.ts +202 -0
- package/packages/firestore/src/task-handler.ts +164 -0
- package/packages/firestore/tsconfig.json +19 -0
- package/pnpm-workspace.yaml +2 -0
- package/turbo.json +31 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Melaka Core - Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared utility functions for content processing, hashing, and field detection.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createHash } from 'crypto';
|
|
8
|
+
import type { SchemaType, SeparatedContent } from './types';
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Content Hashing
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate SHA256 hash of content for change detection.
|
|
16
|
+
*
|
|
17
|
+
* @param content - Object to hash
|
|
18
|
+
* @returns Hex-encoded SHA256 hash
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const hash = hashContent({ title: 'Hello', body: 'World' });
|
|
23
|
+
* // 'a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e'
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function hashContent(content: Record<string, unknown>): string {
|
|
27
|
+
const normalized = JSON.stringify(content, Object.keys(content).sort());
|
|
28
|
+
return createHash('sha256').update(normalized).digest('hex');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Field Type Detection
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if a value is a Firestore DocumentReference.
|
|
37
|
+
*/
|
|
38
|
+
function isDocumentReference(value: unknown): boolean {
|
|
39
|
+
return (
|
|
40
|
+
value !== null &&
|
|
41
|
+
typeof value === 'object' &&
|
|
42
|
+
'path' in value &&
|
|
43
|
+
typeof (value as { path: unknown }).path === 'string' &&
|
|
44
|
+
'id' in value
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if a value is a Firestore Timestamp.
|
|
50
|
+
*/
|
|
51
|
+
function isTimestamp(value: unknown): boolean {
|
|
52
|
+
return (
|
|
53
|
+
value !== null &&
|
|
54
|
+
typeof value === 'object' &&
|
|
55
|
+
'_seconds' in value &&
|
|
56
|
+
'_nanoseconds' in value
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if a value is a Firestore GeoPoint.
|
|
62
|
+
*/
|
|
63
|
+
function isGeoPoint(value: unknown): boolean {
|
|
64
|
+
return (
|
|
65
|
+
value !== null &&
|
|
66
|
+
typeof value === 'object' &&
|
|
67
|
+
'latitude' in value &&
|
|
68
|
+
'longitude' in value
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Detect the schema type of a value.
|
|
74
|
+
*
|
|
75
|
+
* @param value - Value to detect type of
|
|
76
|
+
* @returns Detected schema type
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* detectFieldType('hello') // 'string'
|
|
81
|
+
* detectFieldType(['a', 'b']) // 'string[]'
|
|
82
|
+
* detectFieldType(123) // 'number'
|
|
83
|
+
* detectFieldType({ foo: 'bar' }) // 'object'
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export function detectFieldType(value: unknown): SchemaType {
|
|
87
|
+
// Null values
|
|
88
|
+
if (value === null) {
|
|
89
|
+
return 'object|null';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Arrays
|
|
93
|
+
if (Array.isArray(value)) {
|
|
94
|
+
if (value.length === 0) {
|
|
95
|
+
// Empty arrays default to string[] (most common case)
|
|
96
|
+
return 'string[]';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const firstItem = value[0];
|
|
100
|
+
|
|
101
|
+
// Check for DocumentReference array
|
|
102
|
+
if (isDocumentReference(firstItem)) {
|
|
103
|
+
return 'DocumentReference[]';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check for string array
|
|
107
|
+
if (typeof firstItem === 'string') {
|
|
108
|
+
return 'string[]';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check for number array
|
|
112
|
+
if (typeof firstItem === 'number') {
|
|
113
|
+
return 'number[]';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Object array
|
|
117
|
+
if (typeof firstItem === 'object' && firstItem !== null) {
|
|
118
|
+
return 'object[]';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Default to string[] for other arrays
|
|
122
|
+
return 'string[]';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// DocumentReference
|
|
126
|
+
if (isDocumentReference(value)) {
|
|
127
|
+
return 'DocumentReference';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Timestamp
|
|
131
|
+
if (isTimestamp(value)) {
|
|
132
|
+
return 'object'; // Timestamps are copied, not translated
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// GeoPoint
|
|
136
|
+
if (isGeoPoint(value)) {
|
|
137
|
+
return 'object'; // GeoPoints are copied, not translated
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Primitives
|
|
141
|
+
if (typeof value === 'string') {
|
|
142
|
+
return 'string';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (typeof value === 'number') {
|
|
146
|
+
return 'number';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (typeof value === 'boolean') {
|
|
150
|
+
return 'boolean';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Objects
|
|
154
|
+
if (typeof value === 'object') {
|
|
155
|
+
return 'object';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Default fallback
|
|
159
|
+
return 'object';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Check if a schema type represents translatable content.
|
|
164
|
+
*
|
|
165
|
+
* Only strings and string arrays are sent to AI for translation.
|
|
166
|
+
* Everything else is copied as-is.
|
|
167
|
+
*
|
|
168
|
+
* @param schemaType - Schema type to check
|
|
169
|
+
* @returns Whether the type is translatable
|
|
170
|
+
*/
|
|
171
|
+
export function isTranslatableType(schemaType: SchemaType): boolean {
|
|
172
|
+
return schemaType === 'string' || schemaType === 'string[]';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ============================================================================
|
|
176
|
+
// Content Separation
|
|
177
|
+
// ============================================================================
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Separate document content into translatable and non-translatable parts.
|
|
181
|
+
*
|
|
182
|
+
* @param doc - Document data
|
|
183
|
+
* @param fields - Optional list of fields to consider (if omitted, all fields are considered)
|
|
184
|
+
* @returns Separated content with detected types
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```typescript
|
|
188
|
+
* const result = separateContent({
|
|
189
|
+
* title: 'Hello',
|
|
190
|
+
* price: 99,
|
|
191
|
+
* tags: ['sale', 'new'],
|
|
192
|
+
* author_ref: docRef,
|
|
193
|
+
* });
|
|
194
|
+
*
|
|
195
|
+
* // result.translatable = { title: 'Hello', tags: ['sale', 'new'] }
|
|
196
|
+
* // result.nonTranslatable = { price: 99, author_ref: docRef }
|
|
197
|
+
* // result.detectedTypes = { title: 'string', price: 'number', ... }
|
|
198
|
+
* ```
|
|
199
|
+
*/
|
|
200
|
+
export function separateContent(
|
|
201
|
+
doc: Record<string, unknown>,
|
|
202
|
+
fields?: string[]
|
|
203
|
+
): SeparatedContent {
|
|
204
|
+
const translatable: Record<string, unknown> = {};
|
|
205
|
+
const nonTranslatable: Record<string, unknown> = {};
|
|
206
|
+
const detectedTypes: Record<string, SchemaType> = {};
|
|
207
|
+
|
|
208
|
+
for (const [field, value] of Object.entries(doc)) {
|
|
209
|
+
// Skip internal Melaka metadata
|
|
210
|
+
if (field === '_melaka') {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// If fields list is provided, only process those fields
|
|
215
|
+
if (fields && !fields.includes(field)) {
|
|
216
|
+
nonTranslatable[field] = value;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const schemaType = detectFieldType(value);
|
|
221
|
+
detectedTypes[field] = schemaType;
|
|
222
|
+
|
|
223
|
+
if (isTranslatableType(schemaType)) {
|
|
224
|
+
translatable[field] = value;
|
|
225
|
+
} else {
|
|
226
|
+
nonTranslatable[field] = value;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return { translatable, nonTranslatable, detectedTypes };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ============================================================================
|
|
234
|
+
// Glossary Utilities
|
|
235
|
+
// ============================================================================
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Merge shared and collection-specific glossaries.
|
|
239
|
+
*
|
|
240
|
+
* Collection glossary takes precedence over shared glossary.
|
|
241
|
+
*
|
|
242
|
+
* @param shared - Shared glossary (applies to all collections)
|
|
243
|
+
* @param collection - Collection-specific glossary
|
|
244
|
+
* @returns Merged glossary
|
|
245
|
+
*/
|
|
246
|
+
export function mergeGlossaries(
|
|
247
|
+
shared?: Record<string, string>,
|
|
248
|
+
collection?: Record<string, string>
|
|
249
|
+
): Record<string, string> {
|
|
250
|
+
return {
|
|
251
|
+
...(shared || {}),
|
|
252
|
+
...(collection || {}),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Format glossary for inclusion in AI prompt.
|
|
258
|
+
*
|
|
259
|
+
* @param glossary - Glossary to format
|
|
260
|
+
* @returns Formatted string for prompt
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* ```typescript
|
|
264
|
+
* formatGlossary({ 'checkout': 'checkout', 'cart': 'troli' })
|
|
265
|
+
* // 'checkout → checkout\ncart → troli'
|
|
266
|
+
* ```
|
|
267
|
+
*/
|
|
268
|
+
export function formatGlossary(glossary?: Record<string, string>): string {
|
|
269
|
+
if (!glossary || Object.keys(glossary).length === 0) {
|
|
270
|
+
return 'No specific glossary terms.';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return Object.entries(glossary)
|
|
274
|
+
.map(([term, translation]) => `${term} → ${translation}`)
|
|
275
|
+
.join('\n');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ============================================================================
|
|
279
|
+
// Language Utilities
|
|
280
|
+
// ============================================================================
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Get display name for a language code.
|
|
284
|
+
*
|
|
285
|
+
* @param code - BCP 47 language code
|
|
286
|
+
* @returns Human-readable language name
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* ```typescript
|
|
290
|
+
* getLanguageName('ms-MY') // 'Malay (Malaysia)'
|
|
291
|
+
* getLanguageName('zh-CN') // 'Chinese (Simplified)'
|
|
292
|
+
* ```
|
|
293
|
+
*/
|
|
294
|
+
export function getLanguageName(code: string): string {
|
|
295
|
+
const names: Record<string, string> = {
|
|
296
|
+
'ms-MY': 'Malay (Malaysia)',
|
|
297
|
+
'zh-CN': 'Chinese (Simplified)',
|
|
298
|
+
'zh-TW': 'Chinese (Traditional)',
|
|
299
|
+
'ta-IN': 'Tamil (India)',
|
|
300
|
+
'hi-IN': 'Hindi (India)',
|
|
301
|
+
'id-ID': 'Indonesian',
|
|
302
|
+
'th-TH': 'Thai',
|
|
303
|
+
'vi-VN': 'Vietnamese',
|
|
304
|
+
'ja-JP': 'Japanese',
|
|
305
|
+
'ko-KR': 'Korean',
|
|
306
|
+
'es-ES': 'Spanish (Spain)',
|
|
307
|
+
'es-MX': 'Spanish (Mexico)',
|
|
308
|
+
'fr-FR': 'French (France)',
|
|
309
|
+
'de-DE': 'German',
|
|
310
|
+
'pt-BR': 'Portuguese (Brazil)',
|
|
311
|
+
'pt-PT': 'Portuguese (Portugal)',
|
|
312
|
+
'ar-SA': 'Arabic (Saudi Arabia)',
|
|
313
|
+
'ru-RU': 'Russian',
|
|
314
|
+
'it-IT': 'Italian',
|
|
315
|
+
'nl-NL': 'Dutch',
|
|
316
|
+
'pl-PL': 'Polish',
|
|
317
|
+
'tr-TR': 'Turkish',
|
|
318
|
+
'en-US': 'English (US)',
|
|
319
|
+
'en-GB': 'English (UK)',
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
return names[code] || code;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ============================================================================
|
|
326
|
+
// ID Generation
|
|
327
|
+
// ============================================================================
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Generate a unique batch ID for translation operations.
|
|
331
|
+
*
|
|
332
|
+
* @returns Unique batch ID
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* ```typescript
|
|
336
|
+
* generateBatchId() // 'batch_1708972800000_a1b2c3'
|
|
337
|
+
* ```
|
|
338
|
+
*/
|
|
339
|
+
export function generateBatchId(): string {
|
|
340
|
+
const timestamp = Date.now();
|
|
341
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
342
|
+
return `batch_${timestamp}_${random}`;
|
|
343
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"declarationMap": true,
|
|
13
|
+
"sourceMap": true,
|
|
14
|
+
"outDir": "./dist",
|
|
15
|
+
"rootDir": "./src"
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# @melaka/firestore
|
|
2
|
+
|
|
3
|
+
Firestore adapter and triggers for Melaka - AI-powered Firestore localization.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @melaka/firestore
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### i18n Operations
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { readTranslation, writeTranslation, getI18nRef } from '@melaka/firestore';
|
|
17
|
+
|
|
18
|
+
// Read a translation
|
|
19
|
+
const translation = await readTranslation(docRef, 'ms-MY');
|
|
20
|
+
|
|
21
|
+
// Write a translation
|
|
22
|
+
await writeTranslation(docRef, 'ms-MY', translatedContent, nonTranslatable, metadata);
|
|
23
|
+
|
|
24
|
+
// Check if translation is current
|
|
25
|
+
const isCurrent = await isTranslationCurrent(docRef, 'ms-MY', sourceHash);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Translation Processing
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { processTranslation } from '@melaka/firestore';
|
|
32
|
+
|
|
33
|
+
const result = await processTranslation(
|
|
34
|
+
docRef,
|
|
35
|
+
docData,
|
|
36
|
+
'ms-MY',
|
|
37
|
+
config,
|
|
38
|
+
collectionConfig
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (result.success) {
|
|
42
|
+
console.log('Translation completed!');
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Code Generation
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { generateTriggers } from '@melaka/firestore';
|
|
50
|
+
|
|
51
|
+
const files = generateTriggers(config, {
|
|
52
|
+
outputDir: 'functions/src/melaka',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// files contains triggers.ts, task-handler.ts, index.ts
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Documentation
|
|
59
|
+
|
|
60
|
+
See the [main Melaka documentation](https://github.com/rizahassan/melaka).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@melaka/firestore",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Firestore adapter and triggers for Melaka",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
20
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"lint": "eslint src/",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"clean": "rm -rf dist"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@melaka/core": "workspace:*",
|
|
29
|
+
"@melaka/ai": "workspace:*"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"firebase-admin": "^12.0.0",
|
|
33
|
+
"firebase-functions": "^6.0.0",
|
|
34
|
+
"tsup": "^8.0.0",
|
|
35
|
+
"typescript": "^5.3.0",
|
|
36
|
+
"vitest": "^1.2.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"firebase-admin": ">=11.0.0",
|
|
40
|
+
"firebase-functions": ">=4.0.0"
|
|
41
|
+
},
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/rizahassan/melaka.git",
|
|
45
|
+
"directory": "packages/firestore"
|
|
46
|
+
},
|
|
47
|
+
"license": "MIT"
|
|
48
|
+
}
|