@localazy/cdn-client
Version:
Node.js module that allows you to easily interact with the Localazy CDN.
592 lines (591 loc) • 17.5 kB
JavaScript
/* @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