@tandem-language-exchange/content-store 1.1.2 → 1.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/README.md +106 -20
- package/dist/{chunk-YOREZCXB.js → chunk-UCBZUEUP.js} +220 -29
- package/dist/chunk-UCBZUEUP.js.map +1 -0
- package/dist/{chunk-JBFJU4JA.js → chunk-VRWRAFDK.js} +59 -13
- package/dist/chunk-VRWRAFDK.js.map +1 -0
- package/dist/chunk-XP3USUQC.js +82 -0
- package/dist/chunk-XP3USUQC.js.map +1 -0
- package/dist/chunk-YZSLCPN6.js +272 -0
- package/dist/chunk-YZSLCPN6.js.map +1 -0
- package/dist/client/cli.js +31 -8
- package/dist/client/cli.js.map +1 -1
- package/dist/client/{fetch-bundles.js → fetch-content-bundles.js} +8 -7
- package/dist/client/fetch-content-bundles.js.map +1 -0
- package/dist/client/fetch-translation-bundles.js +36 -0
- package/dist/client/fetch-translation-bundles.js.map +1 -0
- package/dist/{index-DJXkO17k.d.ts → index-Db97SUTy.d.ts} +35 -15
- package/dist/index.d.ts +1 -1
- package/dist/node.browser.js +13 -6
- package/dist/node.browser.js.map +1 -1
- package/dist/node.d.ts +12 -5
- package/dist/node.js +345 -39
- package/dist/node.js.map +1 -1
- package/package.json +23 -11
- package/dist/chunk-JBFJU4JA.js.map +0 -1
- package/dist/chunk-UWGOF36L.js +0 -85
- package/dist/chunk-UWGOF36L.js.map +0 -1
- package/dist/chunk-YOREZCXB.js.map +0 -1
- package/dist/client/fetch-bundles.js.map +0 -1
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
downloadWithRetry,
|
|
4
|
+
getDefaultS3RetryConfig
|
|
5
|
+
} from "./chunk-XP3USUQC.js";
|
|
6
|
+
import {
|
|
7
|
+
allProjects,
|
|
8
|
+
buildCmsObjectKey,
|
|
9
|
+
buildTranslationObjectKey,
|
|
10
|
+
config,
|
|
11
|
+
defaultLocales
|
|
12
|
+
} from "./chunk-YZSLCPN6.js";
|
|
5
13
|
|
|
6
14
|
// src/client/config.ts
|
|
7
15
|
import dotenv from "dotenv";
|
|
@@ -101,25 +109,62 @@ function setNestedAt(target, dottedPath, value) {
|
|
|
101
109
|
}
|
|
102
110
|
setNestedAt(nested, rest, value);
|
|
103
111
|
}
|
|
104
|
-
async function
|
|
112
|
+
async function fetchCmsBundles(store, outputDir, options) {
|
|
105
113
|
const { cms, contentTypes } = options;
|
|
114
|
+
const retry = options.retry ?? getDefaultS3RetryConfig();
|
|
106
115
|
await fs.mkdir(outputDir, { recursive: true });
|
|
107
116
|
const result = {};
|
|
108
117
|
await Promise.all(
|
|
109
118
|
contentTypes.map(async (contentType) => {
|
|
110
|
-
const key =
|
|
111
|
-
const data = await store
|
|
112
|
-
const filePath = path.resolve(
|
|
113
|
-
outputDir,
|
|
114
|
-
`${cms}-${contentType}.json`
|
|
115
|
-
);
|
|
119
|
+
const key = buildCmsObjectKey(cms, contentType);
|
|
120
|
+
const data = await downloadWithRetry(store, key, retry);
|
|
121
|
+
const filePath = path.resolve(outputDir, key);
|
|
116
122
|
await fs.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
117
123
|
result[contentType] = filePath;
|
|
118
124
|
})
|
|
119
125
|
);
|
|
120
126
|
return result;
|
|
121
127
|
}
|
|
122
|
-
async function
|
|
128
|
+
async function fetchTranslationBundles(store, outputDir, options) {
|
|
129
|
+
const { projects, locales } = options;
|
|
130
|
+
const retry = options.retry ?? getDefaultS3RetryConfig();
|
|
131
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
132
|
+
const result = {};
|
|
133
|
+
const localesToSync = locales && locales.length ? locales : defaultLocales;
|
|
134
|
+
await Promise.all(
|
|
135
|
+
projects.map(async (project) => {
|
|
136
|
+
const resources = allProjects[project];
|
|
137
|
+
if (!resources?.length) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const resourceTasks = [];
|
|
141
|
+
for (const resource of resources) {
|
|
142
|
+
for (const loc of localesToSync) {
|
|
143
|
+
const locale = resource.localeMapping && resource.localeMapping[loc] ? resource.localeMapping[loc] : loc;
|
|
144
|
+
const objectKey = buildTranslationObjectKey(
|
|
145
|
+
project,
|
|
146
|
+
resource.fileName,
|
|
147
|
+
locale
|
|
148
|
+
);
|
|
149
|
+
resourceTasks.push({ objectKey });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
await Promise.all(
|
|
153
|
+
resourceTasks.map(async ({ objectKey }) => {
|
|
154
|
+
const data = await downloadWithRetry(store, objectKey, retry);
|
|
155
|
+
const filePath = path.resolve(outputDir, objectKey);
|
|
156
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
157
|
+
if (!result[project]) {
|
|
158
|
+
result[project] = {};
|
|
159
|
+
}
|
|
160
|
+
result[project][objectKey] = filePath;
|
|
161
|
+
})
|
|
162
|
+
);
|
|
163
|
+
})
|
|
164
|
+
);
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
async function queryCmsBundle(outputDir, cms, contentType, options = {}) {
|
|
123
168
|
const filePath = path.resolve(
|
|
124
169
|
outputDir,
|
|
125
170
|
`${cms}-${contentType}.json`
|
|
@@ -158,7 +203,8 @@ async function queryBundle(outputDir, cms, contentType, options = {}) {
|
|
|
158
203
|
|
|
159
204
|
export {
|
|
160
205
|
config2 as config,
|
|
161
|
-
|
|
162
|
-
|
|
206
|
+
fetchCmsBundles,
|
|
207
|
+
fetchTranslationBundles,
|
|
208
|
+
queryCmsBundle
|
|
163
209
|
};
|
|
164
|
-
//# sourceMappingURL=chunk-
|
|
210
|
+
//# sourceMappingURL=chunk-VRWRAFDK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client/config.ts","../src/shared/bundles.ts","../src/shared/trimDepth.ts"],"sourcesContent":["import dotenv from 'dotenv';\nimport type { S3Config, CMSProvider } from '../shared/types';\nimport {SharedConfig, config as sharedConfig} from '../shared/config';\n\ndotenv.config({ path: '.env.local' });\ndotenv.config();\n\nexport type { CMSProvider, S3Config };\n\nexport const config:SharedConfig = {\n ...sharedConfig\n};\n","import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport type { CMSProvider } from './types';\nimport { buildCmsObjectKey, buildTranslationObjectKey, ContentStore } from './s3';\nimport {\n downloadWithRetry,\n getDefaultS3RetryConfig,\n type S3RetryConfig,\n} from './s3-retry';\nimport { trimDepth } from './trimDepth';\nimport { allProjects, defaultLocales } from './lingohub';\n\nexport { trimDepth } from './trimDepth';\nexport type { S3RetryConfig } from './s3-retry';\nexport { getDefaultS3RetryConfig } from './s3-retry';\n\nexport interface CmsBundleInfo {\n [key: string]: string;\n}\n\nexport interface TranslationBundleInfo {\n [key: string] : Record<string, string>;\n}\n\nexport interface BundleItem {\n [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any\n}\n\nexport interface FetchCmsBundlesOptions {\n cms: CMSProvider;\n contentTypes: string[];\n /** S3 GET retry; defaults via {@link getDefaultS3RetryConfig}. */\n retry?: S3RetryConfig;\n}\n\nexport interface FetchTranslationBundlesOptions {\n projects: string[];\n locales?: string[] | undefined;\n /** S3 GET retry; defaults via {@link getDefaultS3RetryConfig}. */\n retry?: S3RetryConfig;\n}\n\nexport interface QueryOptions {\n /**\n * Filter items by matching property values (exact equality, or array for IN).\n * Keys may use dot notation for nested paths, e.g. `{ 'meta._id': 'abc' }` matches\n * `item.meta._id`.\n *\n * Contentful-style operators on the path (before `[`):\n * - `{ 'field[exists]': true }` — field is present and non-empty (not null, undefined,\n * `''`, `[]`, or `{}`).\n * - `{ 'field[exists]': false }` — field is missing or empty (same emptiness rules).\n * Nested paths work, e.g. `{ 'blocks.hero[exists]': false }`.\n */\n fields?: Record<string, unknown>;\n /**\n * Properties to include in each result object. Omit to return all properties.\n * Keys may use dot notation; nested segments become nested objects in the result,\n * e.g. `['slug', 'meta._updatedAt']` → `{ slug, meta: { _updatedAt } }`.\n */\n select?: string[];\n /** Maximum number of items to return. */\n limit?: number;\n /**\n * How many levels deep to return.\n * - 1 = the item's own scalar properties only; nested objects/refs are nulled.\n * - 2 = the item including its direct references; refs inside those are nulled.\n * - 3 = three levels deep, and so on.\n * - Omit to return the full depth.\n */\n include?: number;\n}\n\n/** Read a value at a dotted path; returns whether every segment existed. */\nfunction getAtPath(\n obj: unknown,\n dottedPath: string,\n): { found: true; value: unknown } | { found: false } {\n const parts = dottedPath.split('.').filter((p) => p.length > 0);\n if (parts.length === 0) return { found: false };\n\n let cur: unknown = obj;\n for (const p of parts) {\n if (cur === null || typeof cur !== 'object' || Array.isArray(cur)) {\n return { found: false };\n }\n const rec = cur as Record<string, unknown>;\n if (!(p in rec)) return { found: false };\n cur = rec[p];\n }\n return { found: true, value: cur };\n}\n\nconst FIELD_OP_KEY = /^(.+)\\[([^\\]]+)]$/;\n\nfunction parseFieldKey(key: string): { path: string; operator?: string } {\n const m = FIELD_OP_KEY.exec(key);\n if (!m) return { path: key };\n return { path: m[1]!, operator: m[2]! };\n}\n\n/** True when a bundle value counts as “no content” (aligned with typical CMS “empty”). */\nfunction isEmptyValue(value: unknown): boolean {\n if (value === null || value === undefined) return true;\n if (value === '') return true;\n if (Array.isArray(value) && value.length === 0) {\n return true;\n }\n return typeof value === 'object' &&\n !Array.isArray(value) &&\n Object.keys(value).length === 0;\n\n}\n\nfunction matchesFieldFilter(\n item: BundleItem,\n key: string,\n expected: unknown,\n): boolean {\n const { path, operator } = parseFieldKey(key);\n const at = getAtPath(item, path);\n\n if (operator === 'exists') {\n if (expected !== true && expected !== false) return false;\n const empty = !at.found || isEmptyValue(at.value);\n return !expected ? empty : !empty;\n }\n\n const actual = at.found ? at.value : undefined;\n if (Array.isArray(expected)) return expected.includes(actual);\n return actual === expected;\n}\n\n/** Set `value` on `target` at a dotted path, creating plain objects as needed. */\nfunction setNestedAt(\n target: Record<string, unknown>,\n dottedPath: string,\n value: unknown,\n): void {\n const parts = dottedPath.split('.').filter((p) => p.length > 0);\n if (parts.length === 0) return;\n\n if (parts.length === 1) {\n target[parts[0]!] = value;\n return;\n }\n\n const head = parts[0]!;\n const rest = parts.slice(1).join('.');\n let nested = target[head];\n if (\n nested === null ||\n typeof nested !== 'object' ||\n Array.isArray(nested)\n ) {\n nested = {};\n target[head] = nested;\n }\n setNestedAt(nested as Record<string, unknown>, rest, value);\n}\n\n/**\n * Downloads the latest CMS bundles from S3 and writes them as JSON files to `outputDir`.\n *\n * @returns A map of contentType to absolute file path.\n */\nexport async function fetchCmsBundles(\n store: ContentStore,\n outputDir: string,\n options: FetchCmsBundlesOptions,\n): Promise<Record<string, string>> {\n const { cms, contentTypes } = options;\n const retry = options.retry ?? getDefaultS3RetryConfig();\n await fs.mkdir(outputDir, { recursive: true });\n\n const result: CmsBundleInfo = {};\n\n await Promise.all(\n contentTypes.map(async (contentType) => {\n const key = buildCmsObjectKey(cms, contentType);\n const data = await downloadWithRetry(store, key, retry);\n const filePath = path.resolve(outputDir, key);\n await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');\n result[contentType] = filePath;\n }),\n );\n\n return result;\n}\n\n/**\n * Downloads the latest translation bundles from S3 and writes them as JSON files to `outputDir`.\n *\n * @returns For each project, a map of S3 object key to absolute file path on disk.\n */\nexport async function fetchTranslationBundles(\n store: ContentStore,\n outputDir: string,\n options: FetchTranslationBundlesOptions,\n): Promise<TranslationBundleInfo> {\n const { projects, locales } = options;\n const retry = options.retry ?? getDefaultS3RetryConfig();\n await fs.mkdir(outputDir, { recursive: true });\n\n const result: TranslationBundleInfo = {};\n const localesToSync = locales && locales.length ? locales : defaultLocales;\n\n await Promise.all(\n projects.map(async (project) => {\n const resources = allProjects[project];\n if (!resources?.length) {\n return;\n }\n\n const resourceTasks: { objectKey: string }[] = [];\n for (const resource of resources) {\n for (const loc of localesToSync) {\n const locale =\n resource.localeMapping && resource.localeMapping[loc]\n ? resource.localeMapping[loc]\n : loc;\n const objectKey = buildTranslationObjectKey(\n project,\n resource.fileName,\n locale,\n );\n resourceTasks.push({ objectKey });\n }\n }\n\n await Promise.all(\n resourceTasks.map(async ({ objectKey }) => {\n const data = await downloadWithRetry(store, objectKey, retry);\n const filePath = path.resolve(outputDir, objectKey);\n await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');\n if (!result[project]) {\n result[project] = {};\n }\n result[project][objectKey] = filePath;\n }),\n );\n }),\n );\n\n return result;\n}\n\n/**\n * Queries a previously fetched bundle from the local filesystem.\n */\nexport async function queryCmsBundle(\n outputDir: string,\n cms: CMSProvider,\n contentType: string,\n options: QueryOptions = {},\n): Promise<unknown[]> {\n const filePath = path.resolve(\n outputDir,\n `${cms}-${contentType}.json`,\n );\n const raw = await fs.readFile(filePath, 'utf-8');\n let items:BundleItem[] = JSON.parse(raw) as Record<string, unknown>[];\n\n if (options.fields) {\n const filters = options.fields;\n items = items.filter((item) =>\n Object.entries(filters).every(([key, expected]) =>\n matchesFieldFilter(item, key, expected),\n ),\n );\n }\n\n if (options.limit !== undefined && options.limit > 0) {\n items = items.slice(0, options.limit);\n }\n\n if (options.include !== undefined) {\n items = items.map(\n (item) => trimDepth(item, options.include!) as Record<string, unknown>,\n );\n }\n\n if (options.select?.length) {\n const keys = options.select;\n items = items.map((item) => {\n const picked: Record<string, unknown> = {};\n for (const k of keys) {\n const at = getAtPath(item, k);\n if (at.found) setNestedAt(picked, k, at.value);\n }\n return picked;\n });\n }\n\n return items;\n}\n","/**\n * Trims nested object depth.\n *\n * `remaining` represents how many levels the current object is allowed.\n * - Scalar properties are always kept.\n * - Nested objects / arrays-of-objects consume one level. When `remaining`\n * drops to 1 they are replaced with `null` (no budget left for refs).\n */\nexport function trimDepth(value: unknown, remaining: number): unknown {\n if (value === null || value === undefined || typeof value !== 'object') {\n return value;\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => trimDepth(item, remaining));\n }\n\n const obj = value as Record<string, unknown>;\n const result: Record<string, unknown> = {};\n\n for (const [k, v] of Object.entries(obj)) {\n if (v === null || v === undefined || typeof v !== 'object') {\n result[k] = v;\n } else if (remaining <= 1) {\n result[k] = null;\n } else if (Array.isArray(v)) {\n result[k] = v.map((item) => {\n if (item !== null && typeof item === 'object' && !Array.isArray(item)) {\n return trimDepth(item, remaining - 1);\n }\n return item;\n });\n } else {\n result[k] = trimDepth(v, remaining - 1);\n }\n }\n\n return result;\n}\n"],"mappings":"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","names":["config","path"]}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/shared/s3-retry.ts
|
|
4
|
+
function computeDelay(attempt, baseDelayMs, maxDelayMs) {
|
|
5
|
+
const exponential = baseDelayMs * Math.pow(2, attempt);
|
|
6
|
+
const jitter = Math.random() * baseDelayMs;
|
|
7
|
+
return Math.min(exponential + jitter, maxDelayMs);
|
|
8
|
+
}
|
|
9
|
+
function getDefaultS3RetryConfig() {
|
|
10
|
+
return {
|
|
11
|
+
maxRetries: parseInt(
|
|
12
|
+
process.env.S3_RETRY_MAX_RETRIES ?? process.env.RETRY_MAX_RETRIES ?? "5",
|
|
13
|
+
10
|
|
14
|
+
),
|
|
15
|
+
baseDelayMs: parseInt(
|
|
16
|
+
process.env.S3_RETRY_BASE_DELAY_MS ?? process.env.RETRY_BASE_DELAY_MS ?? "1000",
|
|
17
|
+
10
|
|
18
|
+
),
|
|
19
|
+
maxDelayMs: parseInt(
|
|
20
|
+
process.env.S3_RETRY_MAX_DELAY_MS ?? process.env.RETRY_MAX_DELAY_MS ?? "60000",
|
|
21
|
+
10
|
|
22
|
+
)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function isRetryableS3DownloadError(err) {
|
|
26
|
+
if (err === null || err === void 0) return false;
|
|
27
|
+
if (typeof err === "object") {
|
|
28
|
+
const e = err;
|
|
29
|
+
const status = e.$metadata?.httpStatusCode;
|
|
30
|
+
if (status === 404 || status === 403 || status === 401 || status === 400) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (status !== void 0 && (status === 408 || status === 429 || status === 500 || status === 502 || status === 503 || status === 504)) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
const name = e.name ?? "";
|
|
37
|
+
const code = e.Code ?? "";
|
|
38
|
+
if (/SlowDown|Throttl|Timeout|TooManyRequests|ServiceUnavailable|InternalError/i.test(
|
|
39
|
+
name
|
|
40
|
+
) || /SlowDown|Throttl/i.test(code)) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (err instanceof Error) {
|
|
45
|
+
const m = err.message;
|
|
46
|
+
if (/ECONNRESET|ETIMEDOUT|EPIPE|ECONNREFUSED|socket hang up|getaddrinfo/i.test(
|
|
47
|
+
m
|
|
48
|
+
)) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
async function withS3Retry(fn, { maxRetries, baseDelayMs, maxDelayMs }) {
|
|
55
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
56
|
+
try {
|
|
57
|
+
return await fn();
|
|
58
|
+
} catch (err) {
|
|
59
|
+
if (!isRetryableS3DownloadError(err)) {
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
if (attempt === maxRetries) {
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
const delay = computeDelay(attempt, baseDelayMs, maxDelayMs);
|
|
66
|
+
console.warn(
|
|
67
|
+
` S3 request failed (attempt ${attempt + 1}/${maxRetries + 1}): ${err instanceof Error ? err.message : String(err)}. Retrying in ${Math.round(delay)}ms\u2026`
|
|
68
|
+
);
|
|
69
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
throw new Error("withS3Retry: unreachable");
|
|
73
|
+
}
|
|
74
|
+
async function downloadWithRetry(store, key, cfg) {
|
|
75
|
+
return withS3Retry(() => store.download(key), cfg);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export {
|
|
79
|
+
getDefaultS3RetryConfig,
|
|
80
|
+
downloadWithRetry
|
|
81
|
+
};
|
|
82
|
+
//# sourceMappingURL=chunk-XP3USUQC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/shared/s3-retry.ts"],"sourcesContent":["import type { ContentStore } from './s3';\n\n/** Retry/backoff for S3 reads (aligned with CMS `RetryConfig` shape). */\nexport interface S3RetryConfig {\n maxRetries: number;\n baseDelayMs: number;\n maxDelayMs: number;\n}\n\nfunction computeDelay(\n attempt: number,\n baseDelayMs: number,\n maxDelayMs: number,\n): number {\n const exponential = baseDelayMs * Math.pow(2, attempt);\n const jitter = Math.random() * baseDelayMs;\n return Math.min(exponential + jitter, maxDelayMs);\n}\n\n/**\n * Default S3 download retry policy from env.\n * `S3_RETRY_*` overrides `RETRY_*` when set.\n */\nexport function getDefaultS3RetryConfig(): S3RetryConfig {\n return {\n maxRetries: parseInt(\n process.env.S3_RETRY_MAX_RETRIES ??\n process.env.RETRY_MAX_RETRIES ??\n '5',\n 10,\n ),\n baseDelayMs: parseInt(\n process.env.S3_RETRY_BASE_DELAY_MS ??\n process.env.RETRY_BASE_DELAY_MS ??\n '1000',\n 10,\n ),\n maxDelayMs: parseInt(\n process.env.S3_RETRY_MAX_DELAY_MS ??\n process.env.RETRY_MAX_DELAY_MS ??\n '60000',\n 10,\n ),\n };\n}\n\n/**\n * True when a failed S3 GET may succeed after a short wait (503 Slow Down,\n * transient network, throttling). Never true for definitive client errors (404, etc.).\n */\nexport function isRetryableS3DownloadError(err: unknown): boolean {\n if (err === null || err === undefined) return false;\n\n if (typeof err === 'object') {\n const e = err as {\n name?: string;\n Code?: string;\n $metadata?: { httpStatusCode?: number };\n };\n const status = e.$metadata?.httpStatusCode;\n if (status === 404 || status === 403 || status === 401 || status === 400) {\n return false;\n }\n if (\n status !== undefined &&\n (status === 408 ||\n status === 429 ||\n status === 500 ||\n status === 502 ||\n status === 503 ||\n status === 504)\n ) {\n return true;\n }\n\n const name = e.name ?? '';\n const code = e.Code ?? '';\n if (\n /SlowDown|Throttl|Timeout|TooManyRequests|ServiceUnavailable|InternalError/i.test(\n name,\n ) ||\n /SlowDown|Throttl/i.test(code)\n ) {\n return true;\n }\n }\n\n if (err instanceof Error) {\n const m = err.message;\n if (\n /ECONNRESET|ETIMEDOUT|EPIPE|ECONNREFUSED|socket hang up|getaddrinfo/i.test(\n m,\n )\n ) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Runs `fn` with retries when `isRetryableS3DownloadError` applies; otherwise throws immediately.\n */\nexport async function withS3Retry<T>(\n fn: () => Promise<T>,\n { maxRetries, baseDelayMs, maxDelayMs }: S3RetryConfig,\n): Promise<T> {\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await fn();\n } catch (err) {\n if (!isRetryableS3DownloadError(err)) {\n throw err;\n }\n if (attempt === maxRetries) {\n throw err;\n }\n\n const delay = computeDelay(attempt, baseDelayMs, maxDelayMs);\n console.warn(\n ` S3 request failed (attempt ${attempt + 1}/${maxRetries + 1}): ${\n err instanceof Error ? err.message : String(err)\n }. Retrying in ${Math.round(delay)}ms…`,\n );\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n\n throw new Error('withS3Retry: unreachable');\n}\n\nexport async function downloadWithRetry(\n store: ContentStore,\n key: string,\n cfg: S3RetryConfig,\n): Promise<unknown> {\n return withS3Retry(() => store.download(key), cfg);\n}\n"],"mappings":";;;AASA,SAAS,aACP,SACA,aACA,YACQ;AACR,QAAM,cAAc,cAAc,KAAK,IAAI,GAAG,OAAO;AACrD,QAAM,SAAS,KAAK,OAAO,IAAI;AAC/B,SAAO,KAAK,IAAI,cAAc,QAAQ,UAAU;AAClD;AAMO,SAAS,0BAAyC;AACvD,SAAO;AAAA,IACL,YAAY;AAAA,MACV,QAAQ,IAAI,wBACV,QAAQ,IAAI,qBACZ;AAAA,MACF;AAAA,IACF;AAAA,IACA,aAAa;AAAA,MACX,QAAQ,IAAI,0BACV,QAAQ,IAAI,uBACZ;AAAA,MACF;AAAA,IACF;AAAA,IACA,YAAY;AAAA,MACV,QAAQ,IAAI,yBACV,QAAQ,IAAI,sBACZ;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAMO,SAAS,2BAA2B,KAAuB;AAChE,MAAI,QAAQ,QAAQ,QAAQ,OAAW,QAAO;AAE9C,MAAI,OAAO,QAAQ,UAAU;AAC3B,UAAM,IAAI;AAKV,UAAM,SAAS,EAAE,WAAW;AAC5B,QAAI,WAAW,OAAO,WAAW,OAAO,WAAW,OAAO,WAAW,KAAK;AACxE,aAAO;AAAA,IACT;AACA,QACE,WAAW,WACV,WAAW,OACV,WAAW,OACX,WAAW,OACX,WAAW,OACX,WAAW,OACX,WAAW,MACb;AACA,aAAO;AAAA,IACT;AAEA,UAAM,OAAO,EAAE,QAAQ;AACvB,UAAM,OAAO,EAAE,QAAQ;AACvB,QACE,6EAA6E;AAAA,MAC3E;AAAA,IACF,KACA,oBAAoB,KAAK,IAAI,GAC7B;AACA,aAAO;AAAA,IACT;AAAA,EACF;AAEA,MAAI,eAAe,OAAO;AACxB,UAAM,IAAI,IAAI;AACd,QACE,sEAAsE;AAAA,MACpE;AAAA,IACF,GACA;AACA,aAAO;AAAA,IACT;AAAA,EACF;AAEA,SAAO;AACT;AAKA,eAAsB,YACpB,IACA,EAAE,YAAY,aAAa,WAAW,GAC1B;AACZ,WAAS,UAAU,GAAG,WAAW,YAAY,WAAW;AACtD,QAAI;AACF,aAAO,MAAM,GAAG;AAAA,IAClB,SAAS,KAAK;AACZ,UAAI,CAAC,2BAA2B,GAAG,GAAG;AACpC,cAAM;AAAA,MACR;AACA,UAAI,YAAY,YAAY;AAC1B,cAAM;AAAA,MACR;AAEA,YAAM,QAAQ,aAAa,SAAS,aAAa,UAAU;AAC3D,cAAQ;AAAA,QACN,gCAAgC,UAAU,CAAC,IAAI,aAAa,CAAC,MAC3D,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CACjD,iBAAiB,KAAK,MAAM,KAAK,CAAC;AAAA,MACpC;AACA,YAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,KAAK,CAAC;AAAA,IAC3D;AAAA,EACF;AAEA,QAAM,IAAI,MAAM,0BAA0B;AAC5C;AAEA,eAAsB,kBACpB,OACA,KACA,KACkB;AAClB,SAAO,YAAY,MAAM,MAAM,SAAS,GAAG,GAAG,GAAG;AACnD;","names":[]}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/shared/s3.ts
|
|
4
|
+
import {
|
|
5
|
+
S3Client,
|
|
6
|
+
PutObjectCommand,
|
|
7
|
+
GetObjectCommand
|
|
8
|
+
} from "@aws-sdk/client-s3";
|
|
9
|
+
var ContentStore = class {
|
|
10
|
+
client;
|
|
11
|
+
bucket;
|
|
12
|
+
constructor(cfg) {
|
|
13
|
+
this.client = new S3Client({
|
|
14
|
+
region: cfg.region,
|
|
15
|
+
credentials: {
|
|
16
|
+
accessKeyId: cfg.accessKeyId,
|
|
17
|
+
secretAccessKey: cfg.secretAccessKey
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
this.bucket = cfg.bucket;
|
|
21
|
+
}
|
|
22
|
+
async upload(key, data) {
|
|
23
|
+
await this.client.send(
|
|
24
|
+
new PutObjectCommand({
|
|
25
|
+
Bucket: this.bucket,
|
|
26
|
+
Key: key,
|
|
27
|
+
Body: JSON.stringify(data, null, 2),
|
|
28
|
+
ContentType: "application/json"
|
|
29
|
+
})
|
|
30
|
+
);
|
|
31
|
+
return key;
|
|
32
|
+
}
|
|
33
|
+
async download(key) {
|
|
34
|
+
const response = await this.client.send(
|
|
35
|
+
new GetObjectCommand({ Bucket: this.bucket, Key: key })
|
|
36
|
+
);
|
|
37
|
+
const body = await response.Body?.transformToString();
|
|
38
|
+
if (!body) throw new Error(`Empty response for key: ${key}`);
|
|
39
|
+
return JSON.parse(body);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var buildCmsObjectKey = (cms, contentType) => {
|
|
43
|
+
return `${cms}-${contentType}.json`;
|
|
44
|
+
};
|
|
45
|
+
var buildTranslationObjectKey = (project, fileName, locale) => {
|
|
46
|
+
return `lingohub-${project}.${fileName.replaceAll("[locale]", locale)}`;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// src/shared/lingohub.ts
|
|
50
|
+
var defaultLocales = ["en", "fr", "de", "es", "it", "pt-br", "ru", "zh-hans", "zh-hant", "ko", "ja"];
|
|
51
|
+
var localeMapping = {
|
|
52
|
+
ios: {
|
|
53
|
+
"pt-br": "pt",
|
|
54
|
+
"zh-hans": "zh-Hans",
|
|
55
|
+
"zh-hant": "zh-Hant"
|
|
56
|
+
},
|
|
57
|
+
android: {
|
|
58
|
+
"pt-br": "pt",
|
|
59
|
+
"zh-hans": "zh-Hans-CN",
|
|
60
|
+
"zh-hant": "zh-TW"
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var allProjects = {
|
|
64
|
+
"tandem-(new-website)": [
|
|
65
|
+
{
|
|
66
|
+
fileName: "Website.[locale].json",
|
|
67
|
+
type: "json"
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
"tandem-(website)": [
|
|
71
|
+
{
|
|
72
|
+
fileName: "[locale].json",
|
|
73
|
+
type: "json"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
fileName: "AI.[locale].json",
|
|
77
|
+
type: "json"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
fileName: "languages.[locale].json",
|
|
81
|
+
type: "json"
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
"tandem": [
|
|
85
|
+
{
|
|
86
|
+
fileName: "InfoPlist.[locale].strings",
|
|
87
|
+
type: "strings",
|
|
88
|
+
localeMapping: localeMapping.ios
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
fileName: "Localizable.[locale].strings",
|
|
92
|
+
type: "strings",
|
|
93
|
+
localeMapping: localeMapping.ios
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
fileName: "MainiPad.[locale].strings",
|
|
97
|
+
type: "strings",
|
|
98
|
+
localeMapping: localeMapping.ios
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
fileName: "Main.[locale].strings",
|
|
102
|
+
type: "strings",
|
|
103
|
+
localeMapping: localeMapping.ios
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
"tandem-(android)": [
|
|
107
|
+
{
|
|
108
|
+
fileName: "accessibility_localizable.[locale].xml",
|
|
109
|
+
type: "xml",
|
|
110
|
+
localeMapping: localeMapping.android
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
fileName: "call_localizable.[locale].xml",
|
|
114
|
+
type: "xml",
|
|
115
|
+
localeMapping: localeMapping.android
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
fileName: "cert_localizable.[locale].xml",
|
|
119
|
+
type: "xml",
|
|
120
|
+
localeMapping: localeMapping.android
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
fileName: "chat_localizable.[locale].xml",
|
|
124
|
+
type: "xml",
|
|
125
|
+
localeMapping: localeMapping.android
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
fileName: "checklist_localizable.[locale].xml",
|
|
129
|
+
type: "xml",
|
|
130
|
+
localeMapping: localeMapping.android
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
fileName: "clubs_localizable.[locale].xml",
|
|
134
|
+
type: "xml",
|
|
135
|
+
localeMapping: localeMapping.android
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
fileName: "common_localizable.[locale].xml",
|
|
139
|
+
type: "xml",
|
|
140
|
+
localeMapping: localeMapping.android
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
fileName: "community_localizable.[locale].xml",
|
|
144
|
+
type: "xml",
|
|
145
|
+
localeMapping: localeMapping.android
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
fileName: "correction_localizable.[locale].xml",
|
|
149
|
+
type: "xml",
|
|
150
|
+
localeMapping: localeMapping.android
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
fileName: "country_names.[locale].xml",
|
|
154
|
+
type: "xml",
|
|
155
|
+
localeMapping: localeMapping.android
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
fileName: "emoji_localizable.[locale].xml",
|
|
159
|
+
type: "xml",
|
|
160
|
+
localeMapping: localeMapping.android
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
fileName: "errors_localizable.[locale].xml",
|
|
164
|
+
type: "xml",
|
|
165
|
+
localeMapping: localeMapping.android
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
fileName: "expressions_localizable.[locale].xml",
|
|
169
|
+
type: "xml",
|
|
170
|
+
localeMapping: localeMapping.android
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
fileName: "gif_localizable.[locale].xml",
|
|
174
|
+
type: "xml",
|
|
175
|
+
localeMapping: localeMapping.android
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
fileName: "guidelines_localizable.[locale].xml",
|
|
179
|
+
type: "xml",
|
|
180
|
+
localeMapping: localeMapping.android
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
fileName: "languages_localizable.[locale].xml",
|
|
184
|
+
type: "xml",
|
|
185
|
+
localeMapping: localeMapping.android
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
fileName: "localizable.[locale].xml",
|
|
189
|
+
type: "xml",
|
|
190
|
+
localeMapping: localeMapping.android
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
fileName: "localizable2.[locale].xml",
|
|
194
|
+
type: "xml",
|
|
195
|
+
localeMapping: localeMapping.android
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
fileName: "login_localizable.[locale].xml",
|
|
199
|
+
type: "xml",
|
|
200
|
+
localeMapping: localeMapping.android
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
fileName: "myprofile_localizable.[locale].xml",
|
|
204
|
+
type: "xml",
|
|
205
|
+
localeMapping: localeMapping.android
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
fileName: "onb_localizable.[locale].xml",
|
|
209
|
+
type: "xml",
|
|
210
|
+
localeMapping: localeMapping.android
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
fileName: "parties_localizable.[locale].xml",
|
|
214
|
+
type: "xml",
|
|
215
|
+
localeMapping: localeMapping.android
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
fileName: "pro_localizable.[locale].xml",
|
|
219
|
+
type: "xml",
|
|
220
|
+
localeMapping: localeMapping.android
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
fileName: "pro_screen_localizable.[locale].xml",
|
|
224
|
+
type: "xml",
|
|
225
|
+
localeMapping: localeMapping.android
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
fileName: "push_notification_localizable.[locale].xml",
|
|
229
|
+
type: "xml",
|
|
230
|
+
localeMapping: localeMapping.android
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
fileName: "reporting_localizable.[locale].xml",
|
|
234
|
+
type: "xml",
|
|
235
|
+
localeMapping: localeMapping.android
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
fileName: "translation_localizable.[locale].xml",
|
|
239
|
+
type: "xml",
|
|
240
|
+
localeMapping: localeMapping.android
|
|
241
|
+
}
|
|
242
|
+
],
|
|
243
|
+
"tandem-(web-invites)": [
|
|
244
|
+
{
|
|
245
|
+
fileName: "[locale].json",
|
|
246
|
+
type: "json"
|
|
247
|
+
}
|
|
248
|
+
]
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// src/shared/config.ts
|
|
252
|
+
import dotenv from "dotenv";
|
|
253
|
+
dotenv.config({ path: ".env.local" });
|
|
254
|
+
dotenv.config();
|
|
255
|
+
var config = {
|
|
256
|
+
s3: {
|
|
257
|
+
bucket: process.env.CONTENT_STORE_S3_BUCKET ?? "",
|
|
258
|
+
region: process.env.CONTENT_STORE_S3_REGION ?? "eu-central-1",
|
|
259
|
+
accessKeyId: process.env.AWS_ACCESS_KEY ?? "",
|
|
260
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? ""
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
export {
|
|
265
|
+
config,
|
|
266
|
+
ContentStore,
|
|
267
|
+
buildCmsObjectKey,
|
|
268
|
+
buildTranslationObjectKey,
|
|
269
|
+
defaultLocales,
|
|
270
|
+
allProjects
|
|
271
|
+
};
|
|
272
|
+
//# sourceMappingURL=chunk-YZSLCPN6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/shared/s3.ts","../src/shared/lingohub.ts","../src/shared/config.ts"],"sourcesContent":["import {\n S3Client,\n PutObjectCommand,\n GetObjectCommand,\n} from '@aws-sdk/client-s3';\nimport type { S3Config } from './types';\n\nexport class ContentStore {\n private client: S3Client;\n private bucket: string;\n\n constructor(cfg: S3Config) {\n this.client = new S3Client({\n region: cfg.region,\n credentials: {\n accessKeyId: cfg.accessKeyId,\n secretAccessKey: cfg.secretAccessKey,\n },\n });\n this.bucket = cfg.bucket;\n }\n\n async upload(key: string, data: unknown): Promise<string> {\n await this.client.send(\n new PutObjectCommand({\n Bucket: this.bucket,\n Key: key,\n Body: JSON.stringify(data, null, 2),\n ContentType: 'application/json',\n }),\n );\n return key;\n }\n\n async download(key: string): Promise<unknown> {\n const response = await this.client.send(\n new GetObjectCommand({ Bucket: this.bucket, Key: key }),\n );\n const body = await response.Body?.transformToString();\n if (!body) throw new Error(`Empty response for key: ${key}`);\n return JSON.parse(body);\n }\n}\n\n/** {cms}-{contentType}.json (always points at the latest version) */\nexport const buildCmsObjectKey = (cms: string, contentType: string): string => {\n return `${cms}-${contentType}.json`;\n}\n\nexport const buildTranslationObjectKey = (project:string, fileName: string, locale: string): string => {\n return `lingohub-${project}.${fileName.replaceAll('[locale]', locale)}`;\n}\n","export const defaultLocales = ['en', 'fr', 'de', 'es', 'it', 'pt-br', 'ru', 'zh-hans', 'zh-hant', 'ko', 'ja'];\n\nconst localeMapping = {\n ios: {\n 'pt-br': 'pt',\n 'zh-hans': 'zh-Hans',\n 'zh-hant': 'zh-Hant',\n },\n android: {\n 'pt-br': 'pt',\n 'zh-hans': 'zh-Hans-CN',\n 'zh-hant': 'zh-TW',\n },\n};\n\nexport type LingohubResource = {\n fileName: string;\n type: 'json'|'strings'|'xml';\n localeMapping?: Record<string, string>;\n}\n\nexport const allProjects: Record<string, LingohubResource[]> = {\n 'tandem-(new-website)': [\n {\n fileName: 'Website.[locale].json',\n type: 'json'\n }\n ],\n 'tandem-(website)': [\n {\n fileName: '[locale].json',\n type: 'json'\n },\n {\n fileName: 'AI.[locale].json',\n type: 'json'\n },\n {\n fileName: 'languages.[locale].json',\n type: 'json'\n }\n ],\n 'tandem': [\n {\n fileName: 'InfoPlist.[locale].strings',\n type: 'strings',\n localeMapping: localeMapping.ios,\n },\n {\n fileName: 'Localizable.[locale].strings',\n type: 'strings',\n localeMapping: localeMapping.ios,\n },\n {\n fileName: 'MainiPad.[locale].strings',\n type: 'strings',\n localeMapping: localeMapping.ios,\n },\n {\n fileName: 'Main.[locale].strings',\n type: 'strings',\n localeMapping: localeMapping.ios,\n }\n ],\n 'tandem-(android)': [\n {\n fileName: 'accessibility_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'call_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'cert_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'chat_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'checklist_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'clubs_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'common_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'community_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'correction_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'country_names.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'emoji_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'errors_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'expressions_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'gif_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'guidelines_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'languages_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'localizable2.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'login_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'myprofile_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'onb_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'parties_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'pro_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'pro_screen_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'push_notification_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'reporting_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n },\n {\n fileName: 'translation_localizable.[locale].xml',\n type: 'xml',\n localeMapping: localeMapping.android,\n }\n\n ],\n 'tandem-(web-invites)': [\n {\n fileName: '[locale].json',\n type: 'json'\n }\n ]\n};\n","import dotenv from 'dotenv';\nimport type { S3Config, CMSProvider } from './types';\n\ndotenv.config({ path: '.env.local' });\ndotenv.config();\n\nexport type { CMSProvider, S3Config };\n\nexport interface SharedConfig {\n s3: S3Config;\n}\n\nexport const config: SharedConfig = {\n s3: {\n bucket: process.env.CONTENT_STORE_S3_BUCKET ?? '',\n region: process.env.CONTENT_STORE_S3_REGION ?? 'eu-central-1',\n accessKeyId: process.env.AWS_ACCESS_KEY ?? '',\n secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? '',\n }\n};\n"],"mappings":"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","names":[]}
|
package/dist/client/cli.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
config,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
fetchCmsBundles,
|
|
5
|
+
fetchTranslationBundles,
|
|
6
|
+
queryCmsBundle
|
|
7
|
+
} from "../chunk-VRWRAFDK.js";
|
|
8
|
+
import "../chunk-XP3USUQC.js";
|
|
7
9
|
import {
|
|
8
10
|
ContentStore
|
|
9
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-YZSLCPN6.js";
|
|
10
12
|
|
|
11
13
|
// src/client/cli.ts
|
|
12
14
|
import { Command } from "commander";
|
|
13
15
|
var program = new Command();
|
|
14
16
|
program.name("content-store").description("Sync CMS content to S3").version("1.0.0");
|
|
15
|
-
program.command("fetch").description("Download latest bundles from S3 to the local filesystem").requiredOption("--cms <provider>", "CMS provider: contentful | sanity").requiredOption("--types <types>", "Comma-separated content types to fetch").option("--output <directory>", "Output directory", "./content-cache").action(async (opts) => {
|
|
17
|
+
program.command("fetch-cms").description("Download latest CMS bundles from S3 to the local filesystem").requiredOption("--cms <provider>", "CMS provider: contentful | sanity").requiredOption("--types <types>", "Comma-separated content types to fetch").option("--output <directory>", "Output directory", "./content-cache").action(async (opts) => {
|
|
16
18
|
const cms = opts.cms;
|
|
17
19
|
if (!["contentful", "sanity"].includes(cms)) {
|
|
18
20
|
console.error(`Invalid CMS provider: ${cms}`);
|
|
@@ -21,7 +23,7 @@ program.command("fetch").description("Download latest bundles from S3 to the loc
|
|
|
21
23
|
const contentTypes = opts.types.split(",").map((s) => s.trim());
|
|
22
24
|
const store = new ContentStore(config.s3);
|
|
23
25
|
try {
|
|
24
|
-
const files = await
|
|
26
|
+
const files = await fetchCmsBundles(store, opts.output, {
|
|
25
27
|
cms,
|
|
26
28
|
contentTypes
|
|
27
29
|
});
|
|
@@ -34,7 +36,28 @@ program.command("fetch").description("Download latest bundles from S3 to the loc
|
|
|
34
36
|
process.exit(1);
|
|
35
37
|
}
|
|
36
38
|
});
|
|
37
|
-
program.command("
|
|
39
|
+
program.command("fetch-translations").description("Download latest Translation bundles from S3 to the local filesystem").requiredOption("--projects <projects>", "Comma-separated projects to fetch").option("--locales <locales>", "Comma-separated locales to fetch (omit to fetch all)").option("--output <directory>", "Output directory", "./content-cache").action(async (opts) => {
|
|
40
|
+
const projects = opts.projects.split(",").map((s) => s.trim());
|
|
41
|
+
const locales = opts.locales ? opts.locales.split(",").map((s) => s.trim()) : void 0;
|
|
42
|
+
const store = new ContentStore(config.s3);
|
|
43
|
+
try {
|
|
44
|
+
const files = await fetchTranslationBundles(store, opts.output, {
|
|
45
|
+
projects,
|
|
46
|
+
locales
|
|
47
|
+
});
|
|
48
|
+
console.log("Fetched bundles:");
|
|
49
|
+
for (const [project, pathsByKey] of Object.entries(files)) {
|
|
50
|
+
console.log(` ${project}:`);
|
|
51
|
+
for (const [objectKey, filePath] of Object.entries(pathsByKey)) {
|
|
52
|
+
console.log(` ${objectKey} -> ${filePath}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error("Fetch failed:", err);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
program.command("query-cms").description("Query a previously fetched CMS bundle from the local filesystem").requiredOption("--cms <provider>", "CMS provider: contentful | sanity").requiredOption("--type <type>", "Content type to query").option("--output <directory>", "Directory where bundles are stored", "./content-cache").option("--fields <json>", `Filter by fields (JSON object, e.g. '{"columns":"2"}')`).option("--select <props>", "Comma-separated properties to include in results").option("--limit <n>", "Maximum number of results", parseInt).option("--include <n>", "Depth of nested references to include", parseInt).action(
|
|
38
61
|
async (opts) => {
|
|
39
62
|
const cms = opts.cms;
|
|
40
63
|
if (!["contentful", "sanity"].includes(cms)) {
|
|
@@ -42,7 +65,7 @@ program.command("query").description("Query a previously fetched bundle from the
|
|
|
42
65
|
process.exit(1);
|
|
43
66
|
}
|
|
44
67
|
try {
|
|
45
|
-
const results = await
|
|
68
|
+
const results = await queryCmsBundle(opts.output, cms, opts.type, {
|
|
46
69
|
fields: opts.fields ? JSON.parse(opts.fields) : void 0,
|
|
47
70
|
select: opts.select ? opts.select.split(",").map((s) => s.trim()) : void 0,
|
|
48
71
|
limit: opts.limit,
|
package/dist/client/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/client/cli.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { config, type CMSProvider } from './config';\nimport { ContentStore } from '../shared/s3';\nimport {
|
|
1
|
+
{"version":3,"sources":["../../src/client/cli.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { config, type CMSProvider } from './config';\nimport { ContentStore } from '../shared/s3';\nimport { fetchCmsBundles, fetchTranslationBundles, queryCmsBundle } from '../shared/bundles';\n\nconst program = new Command();\n\nprogram\n .name('content-store')\n .description('Sync CMS content to S3')\n .version('1.0.0');\n\nprogram\n .command('fetch-cms')\n .description('Download latest CMS bundles from S3 to the local filesystem')\n .requiredOption('--cms <provider>', 'CMS provider: contentful | sanity')\n .requiredOption('--types <types>', 'Comma-separated content types to fetch')\n .option('--output <directory>', 'Output directory', './content-cache')\n .action(async (opts: { cms: string; types: string; output: string }) => {\n const cms = opts.cms as CMSProvider;\n\n if (!['contentful', 'sanity'].includes(cms)) {\n console.error(`Invalid CMS provider: ${cms}`);\n process.exit(1);\n }\n\n const contentTypes = opts.types.split(',').map((s) => s.trim());\n const store = new ContentStore(config.s3);\n\n try {\n const files = await fetchCmsBundles(store, opts.output, {\n cms,\n contentTypes,\n });\n\n console.log('Fetched bundles:');\n for (const [type, filePath] of Object.entries(files)) {\n console.log(` ${type} -> ${filePath}`);\n }\n } catch (err) {\n console.error('Fetch failed:', err);\n process.exit(1);\n }\n });\n\nprogram\n .command('fetch-translations')\n .description('Download latest Translation bundles from S3 to the local filesystem')\n .requiredOption('--projects <projects>', 'Comma-separated projects to fetch')\n .option('--locales <locales>', 'Comma-separated locales to fetch (omit to fetch all)')\n .option('--output <directory>', 'Output directory', './content-cache')\n .action(async (opts: { projects: string; locales: string; output: string }) => {\n\n const projects = opts.projects.split(',').map((s) => s.trim());\n const locales = opts.locales ? opts.locales.split(',').map((s) => s.trim()) : undefined;\n const store = new ContentStore(config.s3);\n\n try {\n const files = await fetchTranslationBundles(store, opts.output, {\n projects,\n locales,\n });\n\n console.log('Fetched bundles:');\n for (const [project, pathsByKey] of Object.entries(files)) {\n console.log(` ${project}:`);\n for (const [objectKey, filePath] of Object.entries(pathsByKey)) {\n console.log(` ${objectKey} -> ${filePath}`);\n }\n }\n } catch (err) {\n console.error('Fetch failed:', err);\n process.exit(1);\n }\n });\n\nprogram\n .command('query-cms')\n .description('Query a previously fetched CMS bundle from the local filesystem')\n .requiredOption('--cms <provider>', 'CMS provider: contentful | sanity')\n .requiredOption('--type <type>', 'Content type to query')\n .option('--output <directory>', 'Directory where bundles are stored', './content-cache')\n .option('--fields <json>', 'Filter by fields (JSON object, e.g. \\'{\"columns\":\"2\"}\\')')\n .option('--select <props>', 'Comma-separated properties to include in results')\n .option('--limit <n>', 'Maximum number of results', parseInt)\n .option('--include <n>', 'Depth of nested references to include', parseInt)\n .action(\n async (opts: {\n cms: string;\n type: string;\n output: string;\n fields?: string;\n select?: string;\n limit?: number;\n include?: number;\n }) => {\n const cms = opts.cms as CMSProvider;\n\n if (!['contentful', 'sanity'].includes(cms)) {\n console.error(`Invalid CMS provider: ${cms}`);\n process.exit(1);\n }\n\n try {\n const results = await queryCmsBundle(opts.output, cms, opts.type, {\n fields: opts.fields ? JSON.parse(opts.fields) : undefined,\n select: opts.select\n ? opts.select.split(',').map((s) => s.trim())\n : undefined,\n limit: opts.limit,\n include: opts.include,\n });\n\n console.log(JSON.stringify(results, null, 2));\n } catch (err) {\n console.error('Query failed:', err);\n process.exit(1);\n }\n },\n );\n\nprogram.parse();\n"],"mappings":";;;;;;;;;;;;;AAAA,SAAS,eAAe;AAKxB,IAAM,UAAU,IAAI,QAAQ;AAE5B,QACG,KAAK,eAAe,EACpB,YAAY,wBAAwB,EACpC,QAAQ,OAAO;AAElB,QACK,QAAQ,WAAW,EACnB,YAAY,6DAA6D,EACzE,eAAe,oBAAoB,mCAAmC,EACtE,eAAe,mBAAmB,wCAAwC,EAC1E,OAAO,wBAAwB,oBAAoB,iBAAiB,EACpE,OAAO,OAAO,SAAyD;AACxE,QAAM,MAAM,KAAK;AAEjB,MAAI,CAAC,CAAC,cAAc,QAAQ,EAAE,SAAS,GAAG,GAAG;AAC3C,YAAQ,MAAM,yBAAyB,GAAG,EAAE;AAC5C,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,eAAe,KAAK,MAAM,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;AAC9D,QAAM,QAAQ,IAAI,aAAa,OAAO,EAAE;AAExC,MAAI;AACF,UAAM,QAAQ,MAAM,gBAAgB,OAAO,KAAK,QAAQ;AAAA,MACtD;AAAA,MACA;AAAA,IACF,CAAC;AAED,YAAQ,IAAI,kBAAkB;AAC9B,eAAW,CAAC,MAAM,QAAQ,KAAK,OAAO,QAAQ,KAAK,GAAG;AACpD,cAAQ,IAAI,KAAK,IAAI,OAAO,QAAQ,EAAE;AAAA,IACxC;AAAA,EACF,SAAS,KAAK;AACZ,YAAQ,MAAM,iBAAiB,GAAG;AAClC,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF,CAAC;AAEH,QACK,QAAQ,oBAAoB,EAC5B,YAAY,qEAAqE,EACjF,eAAe,yBAAyB,mCAAmC,EAC3E,OAAO,uBAAuB,sDAAsD,EACpF,OAAO,wBAAwB,oBAAoB,iBAAiB,EACpE,OAAO,OAAO,SAAgE;AAE3E,QAAM,WAAW,KAAK,SAAS,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;AAC7D,QAAM,UAAU,KAAK,UAAU,KAAK,QAAQ,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI;AAC9E,QAAM,QAAQ,IAAI,aAAa,OAAO,EAAE;AAExC,MAAI;AACA,UAAM,QAAQ,MAAM,wBAAwB,OAAO,KAAK,QAAQ;AAAA,MAC5D;AAAA,MACA;AAAA,IACJ,CAAC;AAED,YAAQ,IAAI,kBAAkB;AAC9B,eAAW,CAAC,SAAS,UAAU,KAAK,OAAO,QAAQ,KAAK,GAAG;AACzD,cAAQ,IAAI,KAAK,OAAO,GAAG;AAC3B,iBAAW,CAAC,WAAW,QAAQ,KAAK,OAAO,QAAQ,UAAU,GAAG;AAC9D,gBAAQ,IAAI,OAAO,SAAS,OAAO,QAAQ,EAAE;AAAA,MAC/C;AAAA,IACF;AAAA,EACJ,SAAS,KAAK;AACV,YAAQ,MAAM,iBAAiB,GAAG;AAClC,YAAQ,KAAK,CAAC;AAAA,EAClB;AACJ,CAAC;AAEL,QACG,QAAQ,WAAW,EACnB,YAAY,iEAAiE,EAC7E,eAAe,oBAAoB,mCAAmC,EACtE,eAAe,iBAAiB,uBAAuB,EACvD,OAAO,wBAAwB,sCAAsC,iBAAiB,EACtF,OAAO,mBAAmB,wDAA0D,EACpF,OAAO,oBAAoB,kDAAkD,EAC7E,OAAO,eAAe,6BAA6B,QAAQ,EAC3D,OAAO,iBAAiB,yCAAyC,QAAQ,EACzE;AAAA,EACC,OAAO,SAQD;AACJ,UAAM,MAAM,KAAK;AAEjB,QAAI,CAAC,CAAC,cAAc,QAAQ,EAAE,SAAS,GAAG,GAAG;AAC3C,cAAQ,MAAM,yBAAyB,GAAG,EAAE;AAC5C,cAAQ,KAAK,CAAC;AAAA,IAChB;AAEA,QAAI;AACF,YAAM,UAAU,MAAM,eAAe,KAAK,QAAQ,KAAK,KAAK,MAAM;AAAA,QAChE,QAAQ,KAAK,SAAS,KAAK,MAAM,KAAK,MAAM,IAAI;AAAA,QAChD,QAAQ,KAAK,SACT,KAAK,OAAO,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,IAC1C;AAAA,QACJ,OAAO,KAAK;AAAA,QACZ,SAAS,KAAK;AAAA,MAChB,CAAC;AAED,cAAQ,IAAI,KAAK,UAAU,SAAS,MAAM,CAAC,CAAC;AAAA,IAC9C,SAAS,KAAK;AACZ,cAAQ,MAAM,iBAAiB,GAAG;AAClC,cAAQ,KAAK,CAAC;AAAA,IAChB;AAAA,EACF;AACF;AAEF,QAAQ,MAAM;","names":[]}
|