@codefast/image-loader
Version:
Flexible image loader for Next.js supporting multiple CDN providers
111 lines (110 loc) • 3.93 kB
JavaScript
class ImageLoaderFactory {
loaders = [];
config;
loaderCache = {};
transformCache = {};
maxCacheSize = 1000;
constructor(config = {}){
this.config = {
defaultQuality: 75,
...config
};
}
registerLoader(loader) {
if (this.loaders.some((l)=>l.getName() === loader.getName())) throw new Error(`Loader with name "${loader.getName()}" is already registered`);
this.loaders.push(loader);
this.clearLoaderCache();
}
registerLoaders(loaders) {
for (const loader of loaders)this.registerLoader(loader);
}
unregisterLoader(name) {
const index = this.loaders.findIndex((loader)=>loader.getName() === name);
if (-1 !== index) {
this.loaders.splice(index, 1);
this.clearLoaderCache();
return true;
}
return false;
}
getLoaders() {
return [
...this.loaders
];
}
findLoader(source) {
const domain = this.extractDomain(source);
if (domain in this.loaderCache) return this.loaderCache[domain];
let selectedLoader = null;
if (this.config.domainMappings?.[domain]) {
const mappedLoaderName = this.config.domainMappings[domain];
selectedLoader = this.loaders.find((loader)=>loader.getName() === mappedLoaderName) ?? null;
}
selectedLoader ??= this.loaders.find((loader)=>loader.canHandle(source)) ?? null;
this.cacheLoader(domain, selectedLoader);
return selectedLoader;
}
load(config) {
const cacheKey = this.createTransformCacheKey(config);
if (this.transformCache[cacheKey]) return this.transformCache[cacheKey];
const loader = this.findLoader(config.src);
if (!loader) {
console.warn(`No loader found for URL: ${config.src}. Returning original URL.`);
return config.src;
}
const normalizedConfig = {
...config,
quality: config.quality ?? this.config.defaultQuality
};
const transformedUrl = loader.load(normalizedConfig);
this.cacheTransform(cacheKey, transformedUrl);
return transformedUrl;
}
createNextImageLoader() {
return (params)=>this.load({
quality: params.quality,
src: params.src,
width: params.width
});
}
getStats() {
return {
domainMappings: this.config.domainMappings ?? {},
loaderNames: this.loaders.map((loader)=>loader.getName()),
totalLoaders: this.loaders.length
};
}
clear() {
this.loaders.length = 0;
this.clearLoaderCache();
this.clearTransformCache();
}
extractDomain(url) {
try {
const urlObject = new URL(url);
return urlObject.hostname.toLowerCase();
} catch {
return "";
}
}
cacheLoader(domain, loader) {
if (Object.keys(this.loaderCache).length >= this.maxCacheSize) this.clearLoaderCache();
this.loaderCache[domain] = loader;
}
clearLoaderCache() {
for (const key of Object.keys(this.loaderCache))delete this.loaderCache[key];
}
createTransformCacheKey(config) {
const quality = config.quality ?? this.config.defaultQuality ?? 75;
return `${config.src}|${config.width.toString()}|${quality.toString()}`;
}
cacheTransform(key, transformedUrl) {
if (Object.keys(this.transformCache).length >= this.maxCacheSize) this.clearTransformCache();
this.transformCache[key] = transformedUrl;
}
clearTransformCache() {
for (const key of Object.keys(this.transformCache))delete this.transformCache[key];
}
}
const defaultImageLoaderFactory = new ImageLoaderFactory();
export { ImageLoaderFactory, defaultImageLoaderFactory };