@tandem-language-exchange/content-store 1.2.0 → 1.2.2
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 +81 -13
- package/dist/chunk-4DE47ZJD.js +19 -0
- package/dist/chunk-4DE47ZJD.js.map +1 -0
- package/dist/{chunk-YZSLCPN6.js → chunk-6FXNAJSI.js} +160 -14
- package/dist/chunk-6FXNAJSI.js.map +1 -0
- package/dist/{chunk-UCBZUEUP.js → chunk-BAJT7RMQ.js} +31 -69
- package/dist/chunk-BAJT7RMQ.js.map +1 -0
- package/dist/{chunk-XP3USUQC.js → chunk-EQ3DSPTJ.js} +6 -2
- package/dist/{chunk-XP3USUQC.js.map → chunk-EQ3DSPTJ.js.map} +1 -1
- package/dist/chunk-UPIQFNCR.js +17 -0
- package/dist/chunk-UPIQFNCR.js.map +1 -0
- package/dist/{chunk-VRWRAFDK.js → chunk-VKXN2SE6.js} +79 -24
- package/dist/chunk-VKXN2SE6.js.map +1 -0
- package/dist/client/fetch-content-bundles.js +7 -4
- package/dist/client/fetch-content-bundles.js.map +1 -1
- package/dist/client/fetch-merged-translation-bundles.js +46 -0
- package/dist/client/fetch-merged-translation-bundles.js.map +1 -0
- package/dist/client/fetch-translation-bundles.js +22 -11
- package/dist/client/fetch-translation-bundles.js.map +1 -1
- package/dist/client/query-cms.js +33 -0
- package/dist/client/query-cms.js.map +1 -0
- package/dist/{index-Db97SUTy.d.ts → index-PQ7XN47c.d.ts} +36 -7
- package/dist/index.d.ts +1 -1
- package/dist/node.browser.js +7 -0
- package/dist/node.browser.js.map +1 -1
- package/dist/node.d.ts +9 -3
- package/dist/node.js +237 -15
- package/dist/node.js.map +1 -1
- package/package.json +8 -5
- package/dist/chunk-UCBZUEUP.js.map +0 -1
- package/dist/chunk-VRWRAFDK.js.map +0 -1
- package/dist/chunk-YZSLCPN6.js.map +0 -1
- package/dist/client/cli.js +0 -82
- package/dist/client/cli.js.map +0 -1
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
config
|
|
4
|
+
} from "./chunk-4DE47ZJD.js";
|
|
2
5
|
import {
|
|
3
6
|
ContentStore,
|
|
4
7
|
allProjects,
|
|
5
8
|
buildCmsObjectKey,
|
|
6
9
|
buildTranslationObjectKey,
|
|
7
|
-
|
|
10
|
+
contentTypeForTranslationKey,
|
|
8
11
|
defaultLocales
|
|
9
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-6FXNAJSI.js";
|
|
10
13
|
|
|
11
14
|
// src/server/config.ts
|
|
12
15
|
import dotenv from "dotenv";
|
|
@@ -321,69 +324,20 @@ function createAdapter(cms) {
|
|
|
321
324
|
}
|
|
322
325
|
}
|
|
323
326
|
|
|
324
|
-
// src/shared/utils.ts
|
|
325
|
-
import convert from "xml-js";
|
|
326
|
-
import set from "lodash.set";
|
|
327
|
-
import merge from "lodash.merge";
|
|
328
|
-
var convertXMLToJS = (xml) => {
|
|
329
|
-
const converted = convert.xml2js(xml, {
|
|
330
|
-
ignoreComment: true,
|
|
331
|
-
ignoreDeclaration: true,
|
|
332
|
-
ignoreInstruction: true,
|
|
333
|
-
compact: true
|
|
334
|
-
});
|
|
335
|
-
let mapped = {};
|
|
336
|
-
converted.resources.string.forEach((item) => {
|
|
337
|
-
mapped = {
|
|
338
|
-
...mapped,
|
|
339
|
-
[item._attributes.name]: item._text
|
|
340
|
-
};
|
|
341
|
-
});
|
|
342
|
-
return mapped;
|
|
343
|
-
};
|
|
344
|
-
var parseIOSStrings = (strings) => {
|
|
345
|
-
const parsedObj = {};
|
|
346
|
-
strings.split("\n").filter((line) => line.startsWith('"') && line.endsWith(";")).map((line) => line.trim().slice(0, -1)).forEach((line) => {
|
|
347
|
-
let [key, value] = line.split(" = ");
|
|
348
|
-
if (!key || !value) return;
|
|
349
|
-
key = key.slice(1, -1);
|
|
350
|
-
value = value.slice(1, -1);
|
|
351
|
-
parsedObj[key] = value;
|
|
352
|
-
});
|
|
353
|
-
return parsedObj;
|
|
354
|
-
};
|
|
355
|
-
|
|
356
327
|
// src/server/adapters/lingohub.ts
|
|
357
328
|
var cfg = config2.lingohub;
|
|
358
329
|
var apiUrl = "https://api.lingohub.com/v1/" + cfg.workspace + "/projects/";
|
|
359
|
-
|
|
360
|
-
const urlForResourceLocalised = `${apiUrl}${project}/resources/${resource.fileName}?auth_token=${cfg.authToken}`.replace(
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
if (resource.type === "json") {
|
|
371
|
-
return res.json();
|
|
372
|
-
}
|
|
373
|
-
if (resource.type === "xml") {
|
|
374
|
-
const xml = await res.text();
|
|
375
|
-
return convertXMLToJS(xml);
|
|
376
|
-
}
|
|
377
|
-
if (resource.type === "strings") {
|
|
378
|
-
const strings = await res.text();
|
|
379
|
-
return parseIOSStrings(strings);
|
|
380
|
-
}
|
|
381
|
-
throw new Error("Invalid resource type");
|
|
382
|
-
}).catch((err) => {
|
|
383
|
-
throw err;
|
|
384
|
-
});
|
|
385
|
-
return result;
|
|
386
|
-
};
|
|
330
|
+
async function fetchLingohubResourceRaw(project, resource, locale) {
|
|
331
|
+
const urlForResourceLocalised = `${apiUrl}${project}/resources/${resource.fileName}?auth_token=${cfg.authToken}`.replace(
|
|
332
|
+
"[locale]",
|
|
333
|
+
locale
|
|
334
|
+
);
|
|
335
|
+
const res = await fetch(urlForResourceLocalised, { method: "GET" });
|
|
336
|
+
if (!res.ok) {
|
|
337
|
+
throw new Error(`Failed to fetch resource: ${res.status} - ${res.statusText}`);
|
|
338
|
+
}
|
|
339
|
+
return await res.text();
|
|
340
|
+
}
|
|
387
341
|
|
|
388
342
|
// src/server/sync/engine.ts
|
|
389
343
|
async function syncCmsContent(cms, contentTypes, includeLevels) {
|
|
@@ -444,18 +398,26 @@ async function syncTranslations(projects, locales) {
|
|
|
444
398
|
for (const loc of locales) {
|
|
445
399
|
const locale = resource.localeMapping && resource.localeMapping[loc] ? resource.localeMapping[loc] : loc;
|
|
446
400
|
try {
|
|
447
|
-
const
|
|
448
|
-
const objectKey = buildTranslationObjectKey(
|
|
449
|
-
|
|
450
|
-
|
|
401
|
+
const raw = await fetchLingohubResourceRaw(project, resource, locale);
|
|
402
|
+
const objectKey = buildTranslationObjectKey(
|
|
403
|
+
project,
|
|
404
|
+
resource.fileName,
|
|
405
|
+
locale
|
|
406
|
+
);
|
|
407
|
+
await store.uploadRaw(
|
|
408
|
+
objectKey,
|
|
409
|
+
raw,
|
|
410
|
+
contentTypeForTranslationKey(objectKey)
|
|
411
|
+
);
|
|
412
|
+
const byteLength = Buffer.byteLength(raw, "utf8");
|
|
451
413
|
entries.push({
|
|
452
414
|
project,
|
|
453
415
|
locale,
|
|
454
|
-
itemCount,
|
|
416
|
+
itemCount: byteLength,
|
|
455
417
|
objectKey
|
|
456
418
|
});
|
|
457
419
|
console.log(
|
|
458
|
-
` + ${project} - ${locale}: ${
|
|
420
|
+
` + ${project} - ${locale}: ${byteLength} bytes -> ${objectKey}`
|
|
459
421
|
);
|
|
460
422
|
} catch (err) {
|
|
461
423
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -473,4 +435,4 @@ export {
|
|
|
473
435
|
syncCmsContent,
|
|
474
436
|
syncTranslations
|
|
475
437
|
};
|
|
476
|
-
//# sourceMappingURL=chunk-
|
|
438
|
+
//# sourceMappingURL=chunk-BAJT7RMQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/config.ts","../src/server/adapters/contentful.ts","../src/server/sync/retry.ts","../src/server/adapters/sanity.ts","../src/server/adapters/index.ts","../src/server/adapters/lingohub.ts","../src/server/sync/engine.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 interface ContentfulConfig {\n spaceId: string;\n accessToken: string;\n host: string;\n batchSize: number;\n /** Content types to sync. When empty, all content types in the space are synced. */\n contentTypes: string[];\n /** Max nesting depth when unwrapping resolved entries. Deeper references are dropped. */\n maxDepth: number;\n}\n\nexport interface SanityConfig {\n projectId: string;\n dataset: string;\n token: string;\n apiVersion: string;\n}\n\nexport interface LingohubConfig {\n authToken: string;\n workspace: string;\n}\n\nexport interface RetryConfig {\n maxRetries: number;\n baseDelayMs: number;\n maxDelayMs: number;\n}\n\nexport interface RestApiConfig {\n port: number;\n apiToken: string;\n}\n\n/** Background schedule (same behaviour as `content-store sync` in the CLI). */\nexport interface ScheduledCmsJobConfig {\n enabled: boolean;\n intervalMinutes: number;\n /** When true, run once when the server starts, then on every interval. */\n runOnStart: boolean;\n /** Job kind; only `sync` is implemented. */\n task: string;\n syncCms?: CMSProvider;\n syncTypes?: string[];\n}\n\nexport interface ScheduledTranslationJobConfig {\n enabled: boolean;\n intervalMinutes: number;\n /** When true, run once when the server starts, then on every interval. */\n runOnStart: boolean;\n /** Job kind; only `sync` is implemented. */\n task: string;\n syncProjects?: string[];\n}\n\nfunction parseScheduledCmsJobConfig(): ScheduledCmsJobConfig {\n const intervalMinutes = parseInt(process.env.SCHEDULE_INTERVAL_MINUTES ?? '0', 60);\n const enabled = Number.isFinite(intervalMinutes) && intervalMinutes > 0;\n const runOnStart =\n (process.env.SCHEDULE_RUN_ON_START ?? 'true').toLowerCase() !== 'false';\n const task = (process.env.SCHEDULE_TASK ?? 'sync').trim();\n const syncCms = (process.env.SCHEDULE_SYNC_CMS ?? '').trim() as CMSProvider;\n const rawTypes = process.env.SCHEDULE_SYNC_TYPES?.trim();\n const syncTypes = rawTypes\n ? rawTypes.split(',').map((s) => s.trim()).filter(Boolean)\n : undefined;\n\n return {\n enabled,\n intervalMinutes,\n runOnStart,\n task,\n syncCms: syncCms || undefined,\n syncTypes,\n };\n}\n\nfunction parseScheduledTranslationJobConfig(): ScheduledTranslationJobConfig {\n const intervalMinutes = parseInt(process.env.SCHEDULE_INTERVAL_MINUTES ?? '0', 60);\n const enabled = Number.isFinite(intervalMinutes) && intervalMinutes > 0;\n const runOnStart =\n (process.env.SCHEDULE_RUN_ON_START ?? 'true').toLowerCase() !== 'false';\n const task = (process.env.SCHEDULE_TASK ?? 'sync').trim();\n const rawProjects = process.env.SCHEDULE_SYNC_TRANSLATION_PROJECTS?.trim();\n const syncProjects = rawProjects\n ? rawProjects.split(',').map((s) => s.trim()).filter(Boolean)\n : undefined;\n\n return {\n enabled,\n intervalMinutes,\n runOnStart,\n task,\n syncProjects,\n };\n}\n\nexport interface CleanupConfig {\n enabled: boolean;\n /** Delete versioned S3 objects older than this many days. */\n retentionDays: number;\n}\n\nexport interface SlackConfig {\n enabled: boolean;\n botToken: string;\n signingSecret: string;\n /** Socket Mode app-level token (xapp-…). Required when enabled. */\n appToken: string;\n}\n\nexport interface ServerConfig {\n contentful: ContentfulConfig;\n sanity: SanityConfig;\n lingohub: LingohubConfig;\n retry: RetryConfig;\n api: RestApiConfig;\n scheduledCmsJob: ScheduledCmsJobConfig;\n scheduledTranslationJob: ScheduledTranslationJobConfig;\n cleanup: CleanupConfig;\n slack: SlackConfig;\n}\n\nexport const config: ServerConfig & SharedConfig = {\n ...sharedConfig,\n contentful: {\n spaceId: process.env.CONTENTFUL_SPACE_ID ?? '',\n accessToken: process.env.CONTENTFUL_WEBSITE_TOKEN ?? '',\n host: process.env.CONTENTFUL_HOST ?? 'preview.contentful.com',\n batchSize: 1000,\n maxDepth: 4,\n contentTypes: [\n 'asset','page','longtailPage','customJson','banner','cookieBanner','downloadPage'\n // Add Contentful content type IDs here to limit sync scope.\n ],\n },\n\n sanity: {\n projectId: process.env.SANITY_PROJECT_ID ?? '',\n dataset: process.env.SANITY_DATASET ?? 'main',\n token: process.env.SANITY_API_TOKEN ?? '',\n apiVersion: '2024-01-01',\n },\n\n lingohub: {\n authToken : process.env.LINGOHUB_AUTH_TOKEN ?? '',\n workspace: process.env.LINGOHUB_WORKSPACE ?? '',\n },\n\n retry: {\n maxRetries: parseInt(process.env.RETRY_MAX_RETRIES ?? '5', 10),\n baseDelayMs: parseInt(process.env.RETRY_BASE_DELAY_MS ?? '1000', 10),\n maxDelayMs: parseInt(process.env.RETRY_MAX_DELAY_MS ?? '60000', 10),\n },\n\n api: {\n port: parseInt(process.env.PORT ?? '3010'),\n apiToken: process.env.CONTENT_STORE_API_TOKEN ?? '',\n },\n\n scheduledCmsJob: parseScheduledCmsJobConfig(),\n scheduledTranslationJob: parseScheduledTranslationJobConfig(),\n\n cleanup: {\n enabled: (process.env.CLEANUP_ENABLED ?? 'true').toLowerCase() !== 'false',\n retentionDays: parseInt(process.env.CLEANUP_RETENTION_DAYS ?? '30', 10),\n },\n\n slack: {\n enabled: (process.env.SLACK_BOT_TOKEN ?? '').length > 0,\n botToken: process.env.SLACK_BOT_TOKEN ?? '',\n signingSecret: process.env.SLACK_SIGNING_SECRET ?? '',\n appToken: process.env.SLACK_APP_TOKEN ?? '',\n },\n};\n","import {\n createClient,\n type ContentfulClientApi,\n type ContentTypeCollection,\n type AssetCollection,\n} from 'contentful';\nimport type { ContentfulConfig, RetryConfig } from '../config';\nimport type { CMSAdapter, FetchResult } from './types';\nimport { withRetry } from '../sync/retry';\n\ntype CfCollection = {\n items: CfItem[],\n total: number\n}\n\ntype CfItem = {\n metadata:{\n tags: string[],\n concepts: string[]\n }\n sys:{\n type: string,\n id: string\n space: {\n sys: {\n type: string\n linkType: string\n id: string\n }\n },\n environment: {\n sys: {\n id: string\n type: 'Link',\n linkType: 'Environment'\n }\n },\n contentType: {\n sys: {\n type: 'Link',\n linkType: 'ContentType',\n id: string\n }\n },\n createdBy: {\n sys: {\n type: 'Link',\n linkType: 'User',\n id: string,\n }\n },\n updatedBy: {\n sys: {\n type: 'Link',\n linkType: 'User',\n id: string\n }\n },\n 'revision': number,\n 'createdAt': string,\n 'updatedAt': string,\n 'publishedVersion': string\n },\n fields:{\n [key:string]: unknown\n }\n}\n\n\n/**\n * Recursively unwraps Contentful's { metadata, sys, fields } envelope.\n * `depth` tracks how many entry/asset envelopes deep we are — anything\n * beyond `maxDepth` is dropped to avoid blowing the call stack on\n * circular or extremely deep reference chains.\n *\n * `path` holds objects on the current recursion branch only. That way a\n * shared reference (e.g. the same asset on `image` and `mobileImage`) is\n * unwrapped for each sibling; only true cycles (an object recurring as a\n * descendant of itself) yield `undefined`.\n */\nfunction stripEnvelope(\n value: unknown,\n maxDepth: number,\n depth = 0,\n path = new WeakSet<object>(),\n): unknown {\n if (value === null || typeof value !== 'object') return value;\n\n const obj = value as CfItem;\n\n if (path.has(obj)) return undefined;\n path.add(obj);\n\n try {\n if (Array.isArray(value)) {\n return value.map((item) => stripEnvelope(item, maxDepth, depth, path));\n }\n\n const isEnvelope = 'sys' in obj && 'fields' in obj && typeof obj.fields === 'object';\n\n if (isEnvelope) {\n if (depth >= maxDepth) return undefined;\n\n const fields = obj.fields as Record<string, unknown>;\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(fields)) {\n result[k] = stripEnvelope(v, maxDepth, depth + 1, path);\n }\n\n const sys = obj.sys;\n if (sys) {\n const existingMeta = result.meta;\n const metaBase =\n typeof existingMeta === 'object' &&\n existingMeta !== null &&\n !Array.isArray(existingMeta)\n ? (existingMeta as Record<string, unknown>)\n : {};\n const _contentType = sys.contentType ? sys.contentType.sys.id : 'Asset'\n result.meta = { ...metaBase, _id: sys.id, _contentType, _updatedAt: sys.updatedAt };\n }\n\n return result;\n }\n\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) {\n result[k] = stripEnvelope(v, maxDepth, depth, path);\n }\n return result;\n } finally {\n path.delete(obj);\n }\n}\n\nexport class ContentfulAdapter implements CMSAdapter {\n readonly name = 'contentful';\n private client: ContentfulClientApi<undefined>;\n private batchSize: number;\n private maxDepth: number;\n private allowedTypes: string[];\n private retryConfig: RetryConfig;\n\n constructor(cfg: ContentfulConfig, retryConfig: RetryConfig) {\n this.client = createClient({\n space: cfg.spaceId,\n accessToken: cfg.accessToken,\n host: cfg.host,\n });\n this.batchSize = cfg.batchSize;\n this.maxDepth = cfg.maxDepth;\n this.allowedTypes = cfg.contentTypes;\n this.retryConfig = retryConfig;\n }\n\n async getContentTypes(): Promise<string[]> {\n const response = await withRetry<ContentTypeCollection>(\n () => this.client.getContentTypes(),\n this.retryConfig,\n );\n\n const allTypes = response.items.map((ct) => ct.sys.id);\n\n if (this.allowedTypes.length > 0) {\n return allTypes.filter((t) => this.allowedTypes.includes(t));\n }\n return allTypes;\n }\n\n /**\n * Fetches every entry for a content type using batched pagination.\n * Contentful caps `getEntries` at 1 000 items per call, so we page through\n * with `skip` until all items are collected.\n *\n * The reserved content type `\"asset\"` fetches from `getAssets()` instead.\n */\n async fetchAll(contentType: string, includeLevels: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 = 4): Promise<FetchResult> {\n if (contentType === 'asset') {\n return this.fetchAllAssets();\n }\n\n const allItems: unknown[] = [];\n let skip = 0;\n let total = 0;\n\n do {\n const payload = {\n content_type: contentType,\n limit: this.batchSize,\n skip,\n include: includeLevels,\n };\n const response = await this.client.getEntries(payload) as unknown as CfCollection;\n total = response.total;\n allItems.push(...response.items);\n skip += response.items.length;\n\n if (total > this.batchSize) {\n console.log(\n ` [contentful] ${contentType}: fetched ${allItems.length}/${total}`,\n );\n }\n } while (skip < total);\n\n return {\n contentType,\n items: allItems.map((item) => stripEnvelope(item, this.maxDepth)),\n total,\n };\n }\n\n private async fetchAllAssets(): Promise<FetchResult> {\n const allItems: unknown[] = [];\n let skip = 0;\n let total = 0;\n\n do {\n const response = await withRetry<AssetCollection>(\n () => this.client.getAssets({ limit: this.batchSize, skip }),\n this.retryConfig,\n );\n total = response.total;\n allItems.push(...response.items);\n skip += response.items.length;\n\n if (total > this.batchSize) {\n console.log(\n ` [contentful] asset: fetched ${allItems.length}/${total}`,\n );\n }\n } while (skip < total);\n\n return {\n contentType: 'asset',\n items: allItems.map((item) => stripEnvelope(item, this.maxDepth)),\n total,\n };\n }\n}\n","import type { RetryConfig } from '../config';\n\n/**\n * Inspects an error to determine if it represents an API rate-limit (HTTP 429).\n * Returns the suggested wait time in ms when available, otherwise `0` to signal\n * that the caller should fall back to computed backoff. Returns `null` when the\n * error is *not* a rate-limit error.\n */\nfunction rateLimitDelayMs(err: unknown): number | null {\n const e = err as Record<string, unknown>;\n\n if (e?.status === 429 || e?.statusCode === 429) {\n const reset = (e?.headers as Record<string, string> | undefined)?.[\n 'x-contentful-ratelimit-reset'\n ];\n return reset ? parseFloat(reset) * 1000 : 0;\n }\n\n const resp = e?.response as Record<string, unknown> | undefined;\n if (resp?.status === 429) {\n const headers = resp?.headers as Record<string, string> | undefined;\n const reset = headers?.['x-contentful-ratelimit-reset'];\n return reset ? parseFloat(reset) * 1000 : 0;\n }\n\n return null;\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 * Executes `fn` with automatic retry + exponential backoff.\n * Rate-limit (429) responses are handled specially: if the API provides a\n * Retry-After / reset header, that value is respected instead of computed backoff.\n */\nexport async function withRetry<T>(\n fn: () => Promise<T>,\n { maxRetries, baseDelayMs, maxDelayMs }: RetryConfig,\n): Promise<T> {\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await fn();\n } catch (err) {\n if (attempt === maxRetries) throw err;\n\n const rlDelay = rateLimitDelayMs(err);\n let delay: number;\n\n if (rlDelay !== null) {\n delay =\n rlDelay > 0 ? rlDelay : computeDelay(attempt, baseDelayMs, maxDelayMs);\n console.warn(\n ` Rate limited (attempt ${attempt + 1}/${maxRetries}). ` +\n `Waiting ${Math.round(delay)}ms…`,\n );\n } else {\n delay = computeDelay(attempt, baseDelayMs, maxDelayMs);\n console.warn(\n ` Request failed (attempt ${attempt + 1}/${maxRetries}): ` +\n `${(err as Error).message}. Retrying in ${Math.round(delay)}ms…`,\n );\n }\n\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n\n throw new Error('withRetry: unreachable');\n}\n","import { createClient, type SanityClient } from '@sanity/client';\nimport type { SanityConfig, RetryConfig } from '../config';\nimport type { CMSAdapter, FetchResult } from './types';\nimport { withRetry } from '../sync/retry';\n\nexport class SanityAdapter implements CMSAdapter {\n readonly name = 'sanity';\n private client: SanityClient;\n private retryConfig: RetryConfig;\n\n constructor(cfg: SanityConfig, retryConfig: RetryConfig) {\n this.client = createClient({\n projectId: cfg.projectId,\n dataset: cfg.dataset,\n token: cfg.token,\n apiVersion: cfg.apiVersion,\n useCdn: false,\n });\n this.retryConfig = retryConfig;\n }\n\n async getContentTypes(): Promise<string[]> {\n const types: string[] = await withRetry(\n () => this.client.fetch('array::unique(*[]._type)'),\n this.retryConfig,\n );\n return types.filter(\n (t) => !t.startsWith('system.') && !t.startsWith('sanity.'),\n );\n }\n\n async fetchAll(contentType: string): Promise<FetchResult> {\n const items: unknown[] = await withRetry(\n () => this.client.fetch('*[_type == $type]', { type: contentType }),\n this.retryConfig,\n );\n\n console.log(` [sanity] ${contentType}: fetched ${items.length} items`);\n return { contentType, items, total: items.length };\n }\n}\n","import { config, type CMSProvider } from '../config';\nimport type { CMSAdapter } from './types';\nimport { ContentfulAdapter } from './contentful';\nimport { SanityAdapter } from './sanity';\n\nexport function createAdapter(cms: CMSProvider): CMSAdapter {\n switch (cms) {\n case 'contentful':\n return new ContentfulAdapter(config.contentful, config.retry);\n case 'sanity':\n return new SanityAdapter(config.sanity, config.retry);\n default:\n throw new Error(`Unknown CMS provider: ${cms as string}`);\n }\n}\n\nexport type { CMSAdapter, FetchResult } from './types';\n","import { config } from '../config';\nimport type { LingohubResource } from '../../shared/lingohub';\n\nconst cfg = config.lingohub;\nconst apiUrl = 'https://api.lingohub.com/v1/' + cfg.workspace + '/projects/';\n\n/**\n * Downloads the raw Lingohub resource body (exact bytes as UTF-8 text).\n * Sync uploads this unmodified to S3; conversion happens at fetch time.\n */\nexport async function fetchLingohubResourceRaw(\n project: string,\n resource: LingohubResource,\n locale: string,\n): Promise<string> {\n const urlForResourceLocalised =\n `${apiUrl}${project}/resources/${resource.fileName}?auth_token=${cfg.authToken}`.replace(\n '[locale]',\n locale,\n );\n const res = await fetch(urlForResourceLocalised, { method: 'GET' });\n if (!res.ok) {\n throw new Error(`Failed to fetch resource: ${res.status} - ${res.statusText}`);\n }\n return await res.text();\n}\n","import type { CMSProvider } from '../config';\nimport { config } from '../config';\nimport { createAdapter } from '../adapters';\nimport {buildCmsObjectKey, buildTranslationObjectKey, ContentStore} from '../../shared/s3';\nimport { allProjects, defaultLocales } from '../../shared/lingohub';\nimport { fetchLingohubResourceRaw } from '../adapters/lingohub';\nimport { contentTypeForTranslationKey } from '../../shared/translationResource';\n\nexport interface CmsSyncResultEntry {\n contentType: string;\n itemCount: number;\n objectKey: string;\n}\n\nexport interface CmsSyncResult {\n cms: CMSProvider;\n timestamp: number;\n entries: CmsSyncResultEntry[];\n errors: Array<{ contentType: string; error: string }>;\n}\n\nexport interface TranslationSyncResultEntry {\n project: string;\n locale: string;\n /** UTF-8 byte size of the raw Lingohub file uploaded to S3. */\n itemCount: number;\n objectKey: string;\n}\n\nexport interface TranslationSyncResult {\n timestamp: number;\n entries: TranslationSyncResultEntry[];\n errors: Array<{ locale: string; project: string, error: string }>;\n}\n\nexport async function runSync(cms: CMSProvider, contentTypes?: string[], includeLevels?: number){\n await syncCmsContent(cms, contentTypes, includeLevels )\n}\n\nexport async function syncCmsContent(\n cms: CMSProvider,\n contentTypes?: string[],\n includeLevels?: number\n): Promise<CmsSyncResult> {\n const adapter = createAdapter(cms);\n const store = new ContentStore(config.s3);\n const timestamp = Math.floor(Date.now() / 1000);\n\n console.log(`\\nStarting sync from ${cms} at ${new Date(timestamp * 1000).toISOString()}`);\n\n const typesToSync =\n contentTypes && contentTypes.length > 0\n ? contentTypes\n : await adapter.getContentTypes();\n\n console.log(`Content types to sync: ${typesToSync.join(', ')}\\n`);\n\n const entries: CmsSyncResultEntry[] = [];\n const errors: Array<{ contentType: string; error: string }> = [];\n\n for (const contentType of typesToSync) {\n try {\n const result = await adapter.fetchAll(contentType, includeLevels);\n const objectKey = buildCmsObjectKey(cms, contentType);\n await store.upload(objectKey, result.items);\n\n entries.push({\n contentType,\n itemCount: result.total,\n objectKey,\n });\n\n console.log(\n ` + ${contentType}: ${result.total} items -> ${objectKey}`,\n );\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n errors.push({ contentType, error: message });\n console.error(` x ${contentType}: ${message}`);\n }\n }\n\n console.log(\n `\\nSync complete: ${entries.length} succeeded, ${errors.length} failed\\n`,\n );\n\n return { cms, timestamp, entries, errors };\n}\n\nexport async function syncTranslations(projects?: string[], locales?:string[]):Promise<TranslationSyncResult> {\n\n const store = new ContentStore(config.s3);\n const entries: TranslationSyncResultEntry[] = [];\n const errors: Array<{ project: string; locale: string, error: string }> = [];\n const timestamp = Math.floor(Date.now() / 1000);\n if(!locales){\n locales = defaultLocales;\n }\n if(!projects){\n projects = Object.keys(allProjects);\n }\n\n for(const project of projects) {\n const resources = allProjects[project];\n if(!resources){\n console.error(`No resources found for ${project}`);\n continue;\n }\n for(const resource of resources) {\n for(const loc of locales){\n const locale = (resource.localeMapping && resource.localeMapping[loc]) ? resource.localeMapping[loc] : loc;\n try {\n const raw = await fetchLingohubResourceRaw(project, resource, locale);\n const objectKey = buildTranslationObjectKey(\n project,\n resource.fileName,\n locale,\n );\n await store.uploadRaw(\n objectKey,\n raw,\n contentTypeForTranslationKey(objectKey),\n );\n const byteLength = Buffer.byteLength(raw, 'utf8');\n entries.push({\n project,\n locale,\n itemCount: byteLength,\n objectKey,\n });\n\n console.log(\n ` + ${project} - ${locale}: ${byteLength} bytes -> ${objectKey}`,\n );\n\n // await new Promise(resolve => setTimeout(resolve, 1000)); // Rate limiting\n\n }catch(err){\n const message = err instanceof Error ? err.message : String(err);\n errors.push({ project,locale, error: message });\n console.error(` x ${project} - ${locale}: ${message}`);\n }\n }\n }\n }\n\n return { timestamp, entries, errors };\n}\n"],"mappings":"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","names":["config","result","cfg","createClient","cfg","createClient","config","config","config"]}
|
|
@@ -74,9 +74,13 @@ async function withS3Retry(fn, { maxRetries, baseDelayMs, maxDelayMs }) {
|
|
|
74
74
|
async function downloadWithRetry(store, key, cfg) {
|
|
75
75
|
return withS3Retry(() => store.download(key), cfg);
|
|
76
76
|
}
|
|
77
|
+
async function downloadRawWithRetry(store, key, cfg) {
|
|
78
|
+
return withS3Retry(() => store.downloadRaw(key), cfg);
|
|
79
|
+
}
|
|
77
80
|
|
|
78
81
|
export {
|
|
79
82
|
getDefaultS3RetryConfig,
|
|
80
|
-
downloadWithRetry
|
|
83
|
+
downloadWithRetry,
|
|
84
|
+
downloadRawWithRetry
|
|
81
85
|
};
|
|
82
|
-
//# sourceMappingURL=chunk-
|
|
86
|
+
//# sourceMappingURL=chunk-EQ3DSPTJ.js.map
|
|
@@ -1 +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":[]}
|
|
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\nexport async function downloadRawWithRetry(\n store: ContentStore,\n key: string,\n cfg: S3RetryConfig,\n): Promise<string> {\n return withS3Retry(() => store.downloadRaw(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;AAEA,eAAsB,qBACpB,OACA,KACA,KACiB;AACjB,SAAO,YAAY,MAAM,MAAM,YAAY,GAAG,GAAG,GAAG;AACtD;","names":[]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
config
|
|
4
|
+
} from "./chunk-4DE47ZJD.js";
|
|
5
|
+
|
|
6
|
+
// src/client/config.ts
|
|
7
|
+
import dotenv from "dotenv";
|
|
8
|
+
dotenv.config({ path: ".env.local" });
|
|
9
|
+
dotenv.config();
|
|
10
|
+
var config2 = {
|
|
11
|
+
...config
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
config2 as config
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=chunk-UPIQFNCR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client/config.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"],"mappings":";;;;;;AAAA,OAAO,YAAY;AAInB,OAAO,OAAO,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,OAAO;AAIP,IAAMA,UAAsB;AAAA,EAC/B,GAAG;AACP;","names":["config"]}
|
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
downloadRawWithRetry,
|
|
3
4
|
downloadWithRetry,
|
|
4
5
|
getDefaultS3RetryConfig
|
|
5
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-EQ3DSPTJ.js";
|
|
6
7
|
import {
|
|
7
8
|
allProjects,
|
|
8
9
|
buildCmsObjectKey,
|
|
9
10
|
buildTranslationObjectKey,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import dotenv from "dotenv";
|
|
16
|
-
dotenv.config({ path: ".env.local" });
|
|
17
|
-
dotenv.config();
|
|
18
|
-
var config2 = {
|
|
19
|
-
...config
|
|
20
|
-
};
|
|
11
|
+
defaultLocales,
|
|
12
|
+
parseTranslationResourceRaw,
|
|
13
|
+
toFlatStringMap,
|
|
14
|
+
translationJsonOutputPath
|
|
15
|
+
} from "./chunk-6FXNAJSI.js";
|
|
21
16
|
|
|
22
17
|
// src/shared/bundles.ts
|
|
23
18
|
import fs from "fs/promises";
|
|
@@ -125,6 +120,10 @@ async function fetchCmsBundles(store, outputDir, options) {
|
|
|
125
120
|
);
|
|
126
121
|
return result;
|
|
127
122
|
}
|
|
123
|
+
function filterResources(projectResources, resourceFilter) {
|
|
124
|
+
if (resourceFilter.length === 0) return projectResources;
|
|
125
|
+
return projectResources.filter((r) => resourceFilter.includes(r.resource));
|
|
126
|
+
}
|
|
128
127
|
async function fetchTranslationBundles(store, outputDir, options) {
|
|
129
128
|
const { projects, locales } = options;
|
|
130
129
|
const retry = options.retry ?? getDefaultS3RetryConfig();
|
|
@@ -132,11 +131,11 @@ async function fetchTranslationBundles(store, outputDir, options) {
|
|
|
132
131
|
const result = {};
|
|
133
132
|
const localesToSync = locales && locales.length ? locales : defaultLocales;
|
|
134
133
|
await Promise.all(
|
|
135
|
-
projects.map(async (project) => {
|
|
136
|
-
const
|
|
137
|
-
if (!
|
|
138
|
-
|
|
139
|
-
|
|
134
|
+
Object.entries(projects).map(async ([project, resourceFilter]) => {
|
|
135
|
+
const allResources = allProjects[project];
|
|
136
|
+
if (!allResources?.length) return;
|
|
137
|
+
const resources = filterResources(allResources, resourceFilter);
|
|
138
|
+
if (!resources.length) return;
|
|
140
139
|
const resourceTasks = [];
|
|
141
140
|
for (const resource of resources) {
|
|
142
141
|
for (const loc of localesToSync) {
|
|
@@ -146,14 +145,20 @@ async function fetchTranslationBundles(store, outputDir, options) {
|
|
|
146
145
|
resource.fileName,
|
|
147
146
|
locale
|
|
148
147
|
);
|
|
149
|
-
resourceTasks.push({ objectKey });
|
|
148
|
+
resourceTasks.push({ objectKey, resource });
|
|
150
149
|
}
|
|
151
150
|
}
|
|
152
151
|
await Promise.all(
|
|
153
|
-
resourceTasks.map(async ({ objectKey }) => {
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
|
|
152
|
+
resourceTasks.map(async ({ objectKey, resource }) => {
|
|
153
|
+
const raw = await downloadRawWithRetry(store, objectKey, retry);
|
|
154
|
+
const parsed = parseTranslationResourceRaw(raw, resource);
|
|
155
|
+
const filePath = translationJsonOutputPath(outputDir, objectKey);
|
|
156
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
157
|
+
await fs.writeFile(
|
|
158
|
+
filePath,
|
|
159
|
+
JSON.stringify(parsed, null, 2),
|
|
160
|
+
"utf-8"
|
|
161
|
+
);
|
|
157
162
|
if (!result[project]) {
|
|
158
163
|
result[project] = {};
|
|
159
164
|
}
|
|
@@ -164,6 +169,56 @@ async function fetchTranslationBundles(store, outputDir, options) {
|
|
|
164
169
|
);
|
|
165
170
|
return result;
|
|
166
171
|
}
|
|
172
|
+
async function fetchMergedTranslationBundles(store, outputDir, options) {
|
|
173
|
+
const { projects, locales } = options;
|
|
174
|
+
const retry = options.retry ?? getDefaultS3RetryConfig();
|
|
175
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
176
|
+
const localesToSync = locales && locales.length ? locales : defaultLocales;
|
|
177
|
+
const tasks = [];
|
|
178
|
+
for (const [project, resourceFilter] of Object.entries(projects)) {
|
|
179
|
+
const allResources = allProjects[project];
|
|
180
|
+
if (!allResources?.length) continue;
|
|
181
|
+
const resources = filterResources(allResources, resourceFilter);
|
|
182
|
+
if (!resources.length) continue;
|
|
183
|
+
for (const resource of resources) {
|
|
184
|
+
for (const loc of localesToSync) {
|
|
185
|
+
const mappedLocale = resource.localeMapping && resource.localeMapping[loc] ? resource.localeMapping[loc] : loc;
|
|
186
|
+
const objectKey = buildTranslationObjectKey(
|
|
187
|
+
project,
|
|
188
|
+
resource.fileName,
|
|
189
|
+
mappedLocale
|
|
190
|
+
);
|
|
191
|
+
tasks.push({ catalogLocale: loc, objectKey, resource });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const results = await Promise.all(
|
|
196
|
+
tasks.map(async ({ catalogLocale, objectKey, resource }) => {
|
|
197
|
+
const raw = await downloadRawWithRetry(store, objectKey, retry);
|
|
198
|
+
const parsed = parseTranslationResourceRaw(raw, resource);
|
|
199
|
+
const stringMap = toFlatStringMap(parsed);
|
|
200
|
+
return { catalogLocale, stringMap };
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
const merged = {};
|
|
204
|
+
for (const loc of localesToSync) {
|
|
205
|
+
merged[loc] = {};
|
|
206
|
+
}
|
|
207
|
+
for (const { catalogLocale, stringMap } of results) {
|
|
208
|
+
Object.assign(merged[catalogLocale], stringMap);
|
|
209
|
+
}
|
|
210
|
+
const out = {};
|
|
211
|
+
for (const loc of localesToSync) {
|
|
212
|
+
const filePath = path.resolve(outputDir, `${loc}.json`);
|
|
213
|
+
await fs.writeFile(
|
|
214
|
+
filePath,
|
|
215
|
+
JSON.stringify(merged[loc], null, 2),
|
|
216
|
+
"utf-8"
|
|
217
|
+
);
|
|
218
|
+
out[loc] = filePath;
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
167
222
|
async function queryCmsBundle(outputDir, cms, contentType, options = {}) {
|
|
168
223
|
const filePath = path.resolve(
|
|
169
224
|
outputDir,
|
|
@@ -202,9 +257,9 @@ async function queryCmsBundle(outputDir, cms, contentType, options = {}) {
|
|
|
202
257
|
}
|
|
203
258
|
|
|
204
259
|
export {
|
|
205
|
-
config2 as config,
|
|
206
260
|
fetchCmsBundles,
|
|
207
261
|
fetchTranslationBundles,
|
|
262
|
+
fetchMergedTranslationBundles,
|
|
208
263
|
queryCmsBundle
|
|
209
264
|
};
|
|
210
|
-
//# sourceMappingURL=chunk-
|
|
265
|
+
//# sourceMappingURL=chunk-VKXN2SE6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/shared/bundles.ts","../src/shared/trimDepth.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport type { CMSProvider } from './types';\nimport { buildCmsObjectKey, buildTranslationObjectKey, ContentStore } from './s3';\nimport {\n downloadRawWithRetry,\n downloadWithRetry,\n getDefaultS3RetryConfig,\n type S3RetryConfig,\n} from './s3-retry';\nimport { trimDepth } from './trimDepth';\nimport {\n allProjects,\n defaultLocales,\n type LingohubResource,\n} from './lingohub';\nimport {\n parseTranslationResourceRaw,\n toFlatStringMap,\n translationJsonOutputPath,\n} from './translationResource';\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\n/**\n * Config-file-friendly shape used by `fetchTranslationBundles` and the CLI.\n *\n * ```json\n * {\n * \"projects\": {\n * \"tandem\": [],\n * \"tandem-(website)\": [\"main\", \"ai\"]\n * },\n * \"locales\": [\"en\", \"de\", \"it\"]\n * }\n * ```\n *\n * `projects` maps each Lingohub project id to an array of resource keys.\n * An empty array means **all** resources for that project.\n * `locales` is optional — omit (or pass `[]`) to fetch the default locale set.\n */\nexport interface TranslationFilterConfig {\n projects: Record<string, string[]>;\n locales?: string[];\n}\n\nexport interface FetchTranslationBundlesOptions extends TranslationFilterConfig {\n /** S3 GET retry; defaults via {@link getDefaultS3RetryConfig}. */\n retry?: S3RetryConfig;\n}\n\n/** One merged JSON file per catalog locale (`en.json`, …). Same options as {@link FetchTranslationBundlesOptions}. */\nexport type FetchMergedTranslationBundlesOptions = FetchTranslationBundlesOptions;\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 */\n/** Filter a project's full resource list by the resource keys specified in the config. */\nfunction filterResources(\n projectResources: LingohubResource[],\n resourceFilter: string[],\n): LingohubResource[] {\n if (resourceFilter.length === 0) return projectResources;\n return projectResources.filter((r) => resourceFilter.includes(r.resource));\n}\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 Object.entries(projects).map(async ([project, resourceFilter]) => {\n const allResources = allProjects[project];\n if (!allResources?.length) return;\n\n const resources = filterResources(allResources, resourceFilter);\n if (!resources.length) return;\n\n const resourceTasks: { objectKey: string; resource: LingohubResource }[] =\n [];\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, resource });\n }\n }\n\n await Promise.all(\n resourceTasks.map(async ({ objectKey, resource }) => {\n const raw = await downloadRawWithRetry(store, objectKey, retry);\n const parsed = parseTranslationResourceRaw(raw, resource);\n const filePath = translationJsonOutputPath(outputDir, objectKey);\n await fs.mkdir(path.dirname(filePath), { recursive: true });\n await fs.writeFile(\n filePath,\n JSON.stringify(parsed, null, 2),\n 'utf-8',\n );\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 * Downloads translation bundles from S3, parses each raw Lingohub file, and merges all\n * key/value pairs per **catalog** locale into a single JSON file (e.g. `en.json`).\n * Duplicate keys across resources or projects: **last write wins** (iteration order:\n * projects → resources → locales).\n *\n * @returns Map of locale code to absolute path of the merged `{locale}.json` file.\n */\nexport async function fetchMergedTranslationBundles(\n store: ContentStore,\n outputDir: string,\n options: FetchMergedTranslationBundlesOptions,\n): Promise<Record<string, string>> {\n const { projects, locales } = options;\n const retry = options.retry ?? getDefaultS3RetryConfig();\n await fs.mkdir(outputDir, { recursive: true });\n\n const localesToSync = locales && locales.length ? locales : defaultLocales;\n\n type Task = {\n catalogLocale: string;\n objectKey: string;\n resource: LingohubResource;\n };\n const tasks: Task[] = [];\n\n for (const [project, resourceFilter] of Object.entries(projects)) {\n const allResources = allProjects[project];\n if (!allResources?.length) continue;\n\n const resources = filterResources(allResources, resourceFilter);\n if (!resources.length) continue;\n\n for (const resource of resources) {\n for (const loc of localesToSync) {\n const mappedLocale =\n resource.localeMapping && resource.localeMapping[loc]\n ? resource.localeMapping[loc]\n : loc;\n const objectKey = buildTranslationObjectKey(\n project,\n resource.fileName,\n mappedLocale,\n );\n tasks.push({ catalogLocale: loc, objectKey, resource });\n }\n }\n }\n\n const results = await Promise.all(\n tasks.map(async ({ catalogLocale, objectKey, resource }) => {\n const raw = await downloadRawWithRetry(store, objectKey, retry);\n const parsed = parseTranslationResourceRaw(raw, resource);\n const stringMap = toFlatStringMap(parsed);\n return { catalogLocale, stringMap };\n }),\n );\n\n const merged: Record<string, Record<string, string>> = {};\n for (const loc of localesToSync) {\n merged[loc] = {};\n }\n for (const { catalogLocale, stringMap } of results) {\n Object.assign(merged[catalogLocale]!, stringMap);\n }\n\n const out: Record<string, string> = {};\n for (const loc of localesToSync) {\n const filePath = path.resolve(outputDir, `${loc}.json`);\n await fs.writeFile(\n filePath,\n JSON.stringify(merged[loc], null, 2),\n 'utf-8',\n );\n out[loc] = filePath;\n }\n\n return out;\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":["path"]}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
config
|
|
3
|
+
config
|
|
4
|
+
} from "../chunk-UPIQFNCR.js";
|
|
5
|
+
import {
|
|
4
6
|
fetchCmsBundles
|
|
5
|
-
} from "../chunk-
|
|
6
|
-
import "../chunk-
|
|
7
|
+
} from "../chunk-VKXN2SE6.js";
|
|
8
|
+
import "../chunk-EQ3DSPTJ.js";
|
|
9
|
+
import "../chunk-4DE47ZJD.js";
|
|
7
10
|
import {
|
|
8
11
|
ContentStore
|
|
9
|
-
} from "../chunk-
|
|
12
|
+
} from "../chunk-6FXNAJSI.js";
|
|
10
13
|
|
|
11
14
|
// src/client/fetch-content-bundles.ts
|
|
12
15
|
import { Command } from "commander";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/client/fetch-content-bundles.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { config, type CMSProvider } from './config';\nimport { ContentStore } from '../shared/s3';\nimport { fetchCmsBundles } from '../shared/bundles';\n\nconst program = new Command();\n\nprogram\n .name('fetch-content-bundles')\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.parse();\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/client/fetch-content-bundles.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { config, type CMSProvider } from './config';\nimport { ContentStore } from '../shared/s3';\nimport { fetchCmsBundles } from '../shared/bundles';\n\nconst program = new Command();\n\nprogram\n .name('fetch-content-bundles')\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.parse();\n"],"mappings":";;;;;;;;;;;;;;AAAA,SAAS,eAAe;AAKxB,IAAM,UAAU,IAAI,QAAQ;AAE5B,QACG,KAAK,uBAAuB,EAC5B,YAAY,6DAA6D,EACzE,eAAe,oBAAoB,mCAAmC,EACtE,eAAe,mBAAmB,wCAAwC,EAC1E,OAAO,wBAAwB,oBAAoB,iBAAiB,EACpE,OAAO,OAAO,SAAyD;AACtE,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,QAAQ,MAAM;","names":[]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
config
|
|
4
|
+
} from "../chunk-UPIQFNCR.js";
|
|
5
|
+
import {
|
|
6
|
+
fetchMergedTranslationBundles
|
|
7
|
+
} from "../chunk-VKXN2SE6.js";
|
|
8
|
+
import "../chunk-EQ3DSPTJ.js";
|
|
9
|
+
import "../chunk-4DE47ZJD.js";
|
|
10
|
+
import {
|
|
11
|
+
ContentStore
|
|
12
|
+
} from "../chunk-6FXNAJSI.js";
|
|
13
|
+
|
|
14
|
+
// src/client/fetch-merged-translation-bundles.ts
|
|
15
|
+
import fs from "fs";
|
|
16
|
+
import { Command } from "commander";
|
|
17
|
+
var program = new Command();
|
|
18
|
+
program.name("fetch-merged-translation-bundles").description(
|
|
19
|
+
'Download translation bundles from S3 and write one merged {locale}.json per locale.\n\nAccepts a JSON config file that specifies projects, resources, and locales.\nExample config:\n { "projects": { "tandem": [], "tandem-(website)": ["main","ai"] }, "locales": ["en","de"] }'
|
|
20
|
+
).requiredOption(
|
|
21
|
+
"--config <filepath>",
|
|
22
|
+
"Path to a JSON config file defining projects, resources, and locales"
|
|
23
|
+
).option("--output <directory>", "Output directory", "./content-cache").action(
|
|
24
|
+
async (opts) => {
|
|
25
|
+
let filterConfig;
|
|
26
|
+
try {
|
|
27
|
+
filterConfig = JSON.parse(fs.readFileSync(opts.config, "utf-8"));
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error(`Failed to read config file "${opts.config}":`, err);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
const store = new ContentStore(config.s3);
|
|
33
|
+
try {
|
|
34
|
+
const files = await fetchMergedTranslationBundles(store, opts.output, filterConfig);
|
|
35
|
+
console.log("Wrote merged translation files:");
|
|
36
|
+
for (const [locale, filePath] of Object.entries(files)) {
|
|
37
|
+
console.log(` ${locale}.json -> ${filePath}`);
|
|
38
|
+
}
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error("Fetch failed:", err);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
program.parse();
|
|
46
|
+
//# sourceMappingURL=fetch-merged-translation-bundles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/client/fetch-merged-translation-bundles.ts"],"sourcesContent":["import fs from 'node:fs';\nimport { Command } from 'commander';\nimport { config } from './config';\nimport { ContentStore } from '../shared/s3';\nimport { fetchMergedTranslationBundles, type TranslationFilterConfig } from '../shared/bundles';\n\nconst program = new Command();\n\nprogram\n .name('fetch-merged-translation-bundles')\n .description(\n 'Download translation bundles from S3 and write one merged {locale}.json per locale.\\n\\n' +\n 'Accepts a JSON config file that specifies projects, resources, and locales.\\n' +\n 'Example config:\\n' +\n ' { \"projects\": { \"tandem\": [], \"tandem-(website)\": [\"main\",\"ai\"] }, \"locales\": [\"en\",\"de\"] }',\n )\n .requiredOption(\n '--config <filepath>',\n 'Path to a JSON config file defining projects, resources, and locales',\n )\n .option('--output <directory>', 'Output directory', './content-cache')\n .action(\n async (opts: { config: string; output: string }) => {\n let filterConfig: TranslationFilterConfig;\n try {\n filterConfig = JSON.parse(fs.readFileSync(opts.config, 'utf-8'));\n } catch (err) {\n console.error(`Failed to read config file \"${opts.config}\":`, err);\n process.exit(1);\n }\n\n const store = new ContentStore(config.s3);\n\n try {\n const files = await fetchMergedTranslationBundles(store, opts.output, filterConfig);\n\n console.log('Wrote merged translation files:');\n for (const [locale, filePath] of Object.entries(files)) {\n console.log(` ${locale}.json -> ${filePath}`);\n }\n } catch (err) {\n console.error('Fetch failed:', err);\n process.exit(1);\n }\n },\n );\n\nprogram.parse();\n"],"mappings":";;;;;;;;;;;;;;AAAA,OAAO,QAAQ;AACf,SAAS,eAAe;AAKxB,IAAM,UAAU,IAAI,QAAQ;AAE5B,QACG,KAAK,kCAAkC,EACvC;AAAA,EACC;AAIF,EACC;AAAA,EACC;AAAA,EACA;AACF,EACC,OAAO,wBAAwB,oBAAoB,iBAAiB,EACpE;AAAA,EACC,OAAO,SAA6C;AAClD,QAAI;AACJ,QAAI;AACF,qBAAe,KAAK,MAAM,GAAG,aAAa,KAAK,QAAQ,OAAO,CAAC;AAAA,IACjE,SAAS,KAAK;AACZ,cAAQ,MAAM,+BAA+B,KAAK,MAAM,MAAM,GAAG;AACjE,cAAQ,KAAK,CAAC;AAAA,IAChB;AAEA,UAAM,QAAQ,IAAI,aAAa,OAAO,EAAE;AAExC,QAAI;AACF,YAAM,QAAQ,MAAM,8BAA8B,OAAO,KAAK,QAAQ,YAAY;AAElF,cAAQ,IAAI,iCAAiC;AAC7C,iBAAW,CAAC,QAAQ,QAAQ,KAAK,OAAO,QAAQ,KAAK,GAAG;AACtD,gBAAQ,IAAI,KAAK,MAAM,YAAY,QAAQ,EAAE;AAAA,MAC/C;AAAA,IACF,SAAS,KAAK;AACZ,cAAQ,MAAM,iBAAiB,GAAG;AAClC,cAAQ,KAAK,CAAC;AAAA,IAChB;AAAA,EACF;AACF;AAEF,QAAQ,MAAM;","names":[]}
|
|
@@ -1,25 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
config
|
|
3
|
+
config
|
|
4
|
+
} from "../chunk-UPIQFNCR.js";
|
|
5
|
+
import {
|
|
4
6
|
fetchTranslationBundles
|
|
5
|
-
} from "../chunk-
|
|
6
|
-
import "../chunk-
|
|
7
|
+
} from "../chunk-VKXN2SE6.js";
|
|
8
|
+
import "../chunk-EQ3DSPTJ.js";
|
|
9
|
+
import "../chunk-4DE47ZJD.js";
|
|
7
10
|
import {
|
|
8
11
|
ContentStore
|
|
9
|
-
} from "../chunk-
|
|
12
|
+
} from "../chunk-6FXNAJSI.js";
|
|
10
13
|
|
|
11
14
|
// src/client/fetch-translation-bundles.ts
|
|
15
|
+
import fs from "fs";
|
|
12
16
|
import { Command } from "commander";
|
|
13
17
|
var program = new Command();
|
|
14
|
-
program.name("fetch-translation-bundles").description(
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
program.name("fetch-translation-bundles").description(
|
|
19
|
+
'Download latest translation bundles from S3 to the local filesystem.\n\nAccepts a JSON config file that specifies projects, resources, and locales.\nExample config:\n { "projects": { "tandem": [], "tandem-(website)": ["main","ai"] }, "locales": ["en","de"] }'
|
|
20
|
+
).requiredOption(
|
|
21
|
+
"--config <filepath>",
|
|
22
|
+
"Path to a JSON config file defining projects, resources, and locales"
|
|
23
|
+
).option("--output <directory>", "Output directory", "./content-cache").action(async (opts) => {
|
|
24
|
+
let filterConfig;
|
|
25
|
+
try {
|
|
26
|
+
filterConfig = JSON.parse(fs.readFileSync(opts.config, "utf-8"));
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error(`Failed to read config file "${opts.config}":`, err);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
17
31
|
const store = new ContentStore(config.s3);
|
|
18
32
|
try {
|
|
19
|
-
const files = await fetchTranslationBundles(store, opts.output,
|
|
20
|
-
projects,
|
|
21
|
-
locales
|
|
22
|
-
});
|
|
33
|
+
const files = await fetchTranslationBundles(store, opts.output, filterConfig);
|
|
23
34
|
console.log("Fetched bundles:");
|
|
24
35
|
for (const [project, pathsByKey] of Object.entries(files)) {
|
|
25
36
|
console.log(` ${project}:`);
|