UNPKG

@localazy/cdn-client

Version:

Node.js module that allows you to easily interact with the Localazy CDN.

592 lines (591 loc) 17.5 kB
/* @localazy/cdn-client@1.5.16 * (c) 2026 Localazy <team@localazy.com> * @license MIT */ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); //#region src/cdn/api/api.ts var Api = class { context; constructor(context) { this.context = context; } async fetchLocale(options) { if (this.context.cache.has(options)) return new Promise((resolve) => { resolve(this.context.cache.get(options)); }); return this.context.client.get(options.metafileLocale.uri); } async fetchMetafile() { return await this.context.client.get(this.context.metafile.params.jsonPath); } }; //#endregion //#region src/cdn/cache/memory-cache-adapter.ts var MemoryCacheAdapter = class { map; constructor() { this.map = /* @__PURE__ */ new Map(); } get(key) { return this.map.get(key); } has(key) { return this.map.has(key); } set(key, value) { this.map.set(key, value); } flush() { this.map = /* @__PURE__ */ new Map(); } }; //#endregion //#region src/cdn/utils.ts var isString = (value) => typeof value === "string"; var isUndefined = (value) => typeof value === "undefined"; var isArray = (value) => Array.isArray(value); var isPlainObject = (value) => Object.prototype.toString.call(value) === "[object Object]"; var uniq = (array) => [...new Set(array)]; var uniqBy = (array, predicate) => { const seen = {}; return array.filter((item) => { const key = predicate(item); if (!Object.hasOwn(seen, key)) { seen[key] = true; return true; } return false; }); }; //#endregion //#region src/cdn/cache/locales-cache.ts var LocalesCache = class { context; cacheAdapter; constructor(context) { this.context = context; this.cacheAdapter = new MemoryCacheAdapter(); } setIfMissed(options) { const { metafileFile, metafileLocale, data } = options; const key = this.keyFromMetafile({ metafileFile, metafileLocale }); if (!this.cacheAdapter.has(key)) this.cacheAdapter.set(key, data); } has(options) { const key = this.keyFromMetafile(options); return this.cacheAdapter.has(key); } get(options) { const key = this.keyFromMetafile(options); return this.cacheAdapter.get(key); } flush() { this.cacheAdapter.flush(); } keyFromMetafile(options) { const { metafileFile, metafileLocale } = options; const productFlavors = [...uniq(metafileFile.productFlavors)].sort().join("-"); return [ this.context.metafile.params.cdnId, metafileFile.id, metafileFile.file, metafileFile.path, metafileFile.library, metafileFile.module, metafileFile.buildType, productFlavors, metafileLocale.locale, metafileLocale.timestamp.toString() ].filter((key) => key !== "").join("-"); } }; //#endregion //#region src/cdn/response/response-factory.ts var ResponseFactory = class ResponseFactory { context; constructor(context) { this.context = context; } createCdnResponse(options) { const { requests, responses, hasSingleFileResponse, hasSingleLocaleResponse } = options; if (responses.length === 0 || typeof responses[0] === "undefined") return {}; this.cacheResponses(requests, responses); return hasSingleFileResponse && hasSingleLocaleResponse ? responses[0] : ResponseFactory.transformResponses(options); } cacheResponses(requests, responses) { responses.forEach((response, index) => { if (typeof requests[index] !== "undefined") { const { metafileFile, metafileLocale } = requests[index]; if (metafileFile && metafileLocale) this.context.cache.setIfMissed({ metafileFile, metafileLocale, data: response }); } }); } static transformResponses(options) { const { requests, responses, hasSingleFileResponse } = options; return responses.reduce((acc, cur, index) => { if (typeof requests[index] !== "undefined") { const { metafileFile, metafileLocale } = requests[index]; if (metafileFile && metafileLocale) if (hasSingleFileResponse) acc[metafileLocale.locale] = cur; else { if (!acc[metafileFile.id]) acc[metafileFile.id] = {}; acc[metafileFile.id][metafileLocale.locale] = cur; } } return acc; }, {}); } }; //#endregion //#region src/cdn/context/context.ts var Context = class { metafile; cdn; client; api; cache; responseFactory; constructor(options) { this.metafile = options.metafileContext; this.cdn = options.cdn; this.client = options.client; this.api = new Api(this); this.cache = new LocalesCache(this); this.responseFactory = new ResponseFactory(this); } }; //#endregion //#region src/cdn/metafile/metafile-file.ts var MetafileFile = class { id; file; path; library; module; buildType; timestamp; productFlavors; locales; baseUrl; constructor(options) { this.id = options.id; this.file = options.file; this.path = options.path; this.library = options.library; this.module = options.module; this.buildType = options.buildType; this.timestamp = options.timestamp; this.productFlavors = options.productFlavors; this.locales = options.locales; this.baseUrl = options.baseUrl; } toCdnFile() { return { id: this.id, file: this.file, path: this.path, library: this.library, module: this.module, buildType: this.buildType, productFlavors: this.productFlavors, locales: this.locales.map((locale) => ({ locale: locale.locale, isBaseLocale: locale.isBaseLocale, uri: `${this.baseUrl}${locale.uri}` })) }; } }; //#endregion //#region src/cdn/metafile/metafile-locale.ts var MetafileLocale = class { language; region; script; isRtl; name; localizedName; uri; timestamp; baseLocale; constructor(options, baseLocale) { this.language = options.language; this.region = options.region; this.script = options.script; this.isRtl = options.isRtl; this.name = options.name; this.localizedName = options.localizedName; this.uri = options.uri; this.timestamp = options.timestamp; this.baseLocale = baseLocale; } get locale() { if (this.language && this.region && this.script) return `${this.language}_${this.region}#${this.script}`; if (this.language && this.script) return `${this.language}#${this.script}`; if (this.language && this.region) return `${this.language}_${this.region}`; return this.language; } get isBaseLocale() { return this.locale === this.baseLocale; } toCdnLocale() { return { locale: this.locale, isBaseLocale: this.isBaseLocale, language: this.language, region: this.region, script: this.script, isRtl: this.isRtl, name: this.name, localizedName: this.localizedName }; } }; //#endregion //#region src/cdn/metafile/metafile-data.ts var MetafileData = class MetafileData { projectUrl; baseLocale; locales; timestamp; files; filesMap; constructor(options, params) { this.projectUrl = options.projectUrl; this.timestamp = options.timestamp; this.files = MetafileData.filesFactory(options.files, options.baseLocale, params); this.filesMap = MetafileData.filesMapFactory(this.files); this.locales = MetafileData.localesFactory(this.files); this.baseLocale = this.locales.find((locale) => locale.isBaseLocale); } static createEmpty(params) { return new MetafileData({ projectUrl: "", baseLocale: "", timestamp: 0, files: {} }, params); } static filesFactory(files, baseLocale, params) { return Object.keys(files).reduce((acc, cur) => { if (typeof files[cur] !== "undefined") { const locales = files[cur].locales.map((locale) => new MetafileLocale(locale, baseLocale)); acc.push(new MetafileFile({ ...files[cur], id: cur, locales, baseUrl: params.baseUrl })); } return acc; }, []); } static filesMapFactory(files) { return files.reduce((acc, cur) => { acc[cur.id] = cur; return acc; }, {}); } static localesFactory(files) { return uniqBy(files.reduce((acc, cur) => { acc.push(...cur.locales.map((locale) => locale.toCdnLocale())); return acc; }, []), (cdnLocale) => cdnLocale.locale); } }; //#endregion //#region src/cdn/metafile/metafile-params.ts var MetafileParams = class MetafileParams { options; constructor(metafileUrl) { this.options = MetafileParams.parseMetafileUrl(metafileUrl); } get url() { return this.options.url; } get baseUrl() { return this.options.baseUrl; } get cdnId() { return this.options.cdnId; } get jsonPath() { return this.options.jsonPath; } static parseMetafileUrl(metafileUrl) { let url; try { url = new URL(metafileUrl); } catch { throw new Error("Invalid param: \"options.metafile\" cannot be parsed as url."); } const matches = /^\/(.*?)\/(.*?)\.(v2.json|js|ts)$/.exec(url.pathname); if (matches === null || matches.length !== 4 || typeof matches[1] === "undefined" || typeof matches[2] === "undefined") throw new Error("Invalid param: \"options.metafile\" contains invalid metafile url."); const cdnId = matches[1]; const tagId = matches[2]; return { url: metafileUrl, baseUrl: url.origin, cdnId, jsonPath: `/${cdnId}/${tagId}.v2.json` }; } }; //#endregion //#region src/cdn/context/metafile-context.ts var MetafileContext = class { params; parsedData; data; constructor(options) { this.params = new MetafileParams(options.metafile); this.parsedData = null; this.data = MetafileData.createEmpty(this.params); } setMetafile(metafile) { this.parsedData = metafile; this.data = new MetafileData(metafile, this.params); } }; //#endregion //#region src/cdn/http/fetch-http-adapter.ts var FetchHttpAdapter = class { baseUrl; constructor(baseUrl) { this.baseUrl = baseUrl; } async get(url) { const response = await fetch(`${this.baseUrl}${url}`); const contentType = response.headers.get("content-type"); const isJson = contentType === "application/json5" || contentType === "application/json"; if (response.status >= 400) throw new Error(`Request failed with status code ${response.status.toString()}`); let result; if (isJson) result = await response.json(); else result = await response.text(); return result; } }; //#endregion //#region src/cdn/methods/cdn-base.ts var CdnBase = class { context; constructor(context) { this.context = context; } }; //#endregion //#region src/cdn/methods/cdn-cache.ts var CdnCache = class extends CdnBase { flush = () => { this.context.cache.flush(); }; }; //#endregion //#region src/cdn/methods/cdn-metafile.ts var CdnMetafile = class extends CdnBase { get projectUrl() { return this.context.metafile.data.projectUrl; } get baseLocale() { return this.context.metafile.data.baseLocale; } get url() { return this.context.metafile.params.url; } get files() { return this.context.metafile.data.files.map((file) => file.toCdnFile()); } locales = (options) => { const { excludeBaseLocale } = options || {}; const { locales } = this.context.metafile.data; return excludeBaseLocale ? locales.filter((cdnLocale) => !cdnLocale.isBaseLocale) : locales; }; refresh = async () => { const response = await this.context.api.fetchMetafile(); this.context.metafile.setMetafile(response); }; switch = async (options) => { this.context.metafile.params = new MetafileParams(options.metafile); await this.refresh(); }; }; //#endregion //#region src/cdn/request/locales-map.ts var LocalesMap = class { data; context; constructor(options) { this.context = options.context; this.data = options.data || {}; } }; //#endregion //#region src/cdn/request/request.ts var Request = class { files; localesMap; hasSingleFileResponse; hasSingleLocaleResponse; context; constructor(context) { this.files = []; this.localesMap = new LocalesMap({ context }); this.hasSingleFileResponse = false; this.hasSingleLocaleResponse = false; this.context = context; } async execute() { const payload = this.getPromises(); const promises = payload.map((item) => item[0]); const requests = payload.map((item) => item[1]); const responses = await Promise.all(promises); return this.context.responseFactory.createCdnResponse({ requests, responses, localesMap: this.localesMap, hasSingleFileResponse: this.hasSingleFileResponse, hasSingleLocaleResponse: this.hasSingleLocaleResponse }); } getPromises() { return this.files.reduce((acc, cur) => { if (typeof this.localesMap.data?.[cur.id] !== "undefined") acc.push(...this.localesMap.data[cur.id].map((metafileLocale) => { const request = { metafileFile: cur, metafileLocale }; return [this.context.api.fetchLocale(request), request]; })); return acc; }, []); } }; //#endregion //#region src/cdn/request/request-builder.ts var RequestBuilder = class { context; request; constructor(context) { this.context = context; this.request = new Request(this.context); } addFiles(files) { if (!(isPlainObject(files) || isString(files) || isUndefined(files) || isArray(files))) throw new Error("Invalid param: \"request.files\" must be object, array, string or undefined."); if (isArray(files)) files.forEach((i) => { if (!(isPlainObject(i) || isString(i))) throw new Error("Invalid param: array \"request.files\" must contain objects or strings."); }); if (isString(files)) { this.request.hasSingleFileResponse = true; const file = this.context.metafile.data.files.find((i) => i.id === files); if (!(file instanceof MetafileFile)) throw new Error(`File not found: "${files}".`); this.request.files = [file]; } else if (isUndefined(files)) this.request.files = [...this.context.metafile.data.files]; else if (isArray(files)) this.request.files = files.map((file) => { let metafileFile; if (isString(file)) { const foundFile = this.context.metafile.data.files.find((i) => i.id === file); if (isUndefined(foundFile)) throw new Error(`File not found: "${file}".`); metafileFile = foundFile; } else { const foundFile = this.context.metafile.data.files.find((i) => i.id === file.id); if (isUndefined(foundFile)) throw new Error(`File not found: "${file.id}".`); metafileFile = foundFile; } return metafileFile; }); else if (isPlainObject(files)) { this.request.hasSingleFileResponse = true; const foundFile = this.context.metafile.data.files.find((i) => i.id === files.id); if (isUndefined(foundFile)) throw new Error(`File not found: "${files.id}".`); this.request.files = [foundFile]; } return this; } addLocales(locales, excludeBaseLocale) { if (!(isString(locales) || isUndefined(locales) || isArray(locales))) throw new Error("Invalid param: \"request.locales\" must be array, string or undefined."); if (isArray(locales)) locales.forEach((i) => { if (!isString(i)) throw new Error("Invalid param: array \"request.locales\" must contain strings."); }); if (isString(locales)) { this.request.hasSingleLocaleResponse = true; this.request.files.reduce((acc, cur) => { acc.data[cur.id] = cur.locales.filter((metafileLocale) => metafileLocale.locale === locales); return acc; }, this.request.localesMap); } else if (isUndefined(locales)) this.request.files.reduce((acc, cur) => { acc.data[cur.id] = excludeBaseLocale ? cur.locales.filter((metafileLocale) => !metafileLocale.isBaseLocale) : cur.locales; return acc; }, this.request.localesMap); else if (isArray(locales)) this.request.files.reduce((acc, cur) => { acc.data[cur.id] = cur.locales.filter((metafileLocale) => locales.includes(metafileLocale.locale)); return acc; }, this.request.localesMap); return this; } getCdnRequest() { const result = this.request; this.request = new Request(this.context); return result; } }; //#endregion //#region src/cdn/cdn-client.ts var CdnClient = class CdnClient { metafile; cache; context; static version = "1.5.16"; constructor(options) { const metafileContext = new MetafileContext(options); const client = new FetchHttpAdapter(metafileContext.params.baseUrl); this.context = new Context({ metafileContext, cdn: this, client }); this.metafile = new CdnMetafile(this.context); this.cache = new CdnCache(this.context); } fetch = async (options) => { const { files, locales, excludeBaseLocale } = options || {}; return new RequestBuilder(this.context).addFiles(files).addLocales(locales, excludeBaseLocale).getCdnRequest().execute(); }; static async create(options) { if (!options) throw new Error("Invalid param: missing required \"options\" parameter."); if (!isString(options.metafile)) throw new Error("Invalid param: \"options.metafile\" must be string."); const cdn = new CdnClient(options); await cdn.metafile.refresh(); return cdn; } }; //#endregion exports.Api = Api; exports.CdnBase = CdnBase; exports.CdnCache = CdnCache; exports.CdnClient = CdnClient; exports.CdnMetafile = CdnMetafile; exports.Context = Context; exports.FetchHttpAdapter = FetchHttpAdapter; exports.LocalesCache = LocalesCache; exports.LocalesMap = LocalesMap; exports.MemoryCacheAdapter = MemoryCacheAdapter; exports.MetafileContext = MetafileContext; exports.MetafileData = MetafileData; exports.MetafileFile = MetafileFile; exports.MetafileLocale = MetafileLocale; exports.MetafileParams = MetafileParams; exports.Request = Request; exports.RequestBuilder = RequestBuilder; exports.ResponseFactory = ResponseFactory; exports.isArray = isArray; exports.isPlainObject = isPlainObject; exports.isString = isString; exports.isUndefined = isUndefined; exports.uniq = uniq; exports.uniqBy = uniqBy; //# sourceMappingURL=localazy-cdn-client.cjs.map