refactor(common): remove code duplication in loaders (#47082)

This change introduces common infrastructure for image
loaders. This removes code duplication and makes it
easier to write loaders - a loader author just needs
to provide a loader config to the URL mapping function.

PR Close #47082
This commit is contained in:
Pawel Kozlowski
2022-06-09 12:27:43 +02:00
committed by Pawel Kozlowski
parent bff870db61
commit c23f32c38e
5 changed files with 80 additions and 125 deletions
@@ -6,13 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Provider, ɵRuntimeError as RuntimeError} from '@angular/core';
import {ɵRuntimeError as RuntimeError} from '@angular/core';
import {RuntimeErrorCode} from '../../../errors';
import {PRECONNECT_CHECK_BLOCKLIST} from '../preconnect_link_checker';
import {isValidPath, normalizePath, normalizeSrc} from '../util';
import {normalizeSrc} from '../util';
import {IMAGE_LOADER, ImageLoaderConfig} from './image_loader';
import {createImageLoader, ImageLoaderConfig} from './image_loader';
/**
* Function that generates a built-in ImageLoader for Cloudflare Image Resizing
@@ -24,31 +23,18 @@ import {IMAGE_LOADER, ImageLoaderConfig} from './image_loader';
* e.g. https://mysite.com
* @returns Provider that provides an ImageLoader function
*/
export function provideCloudflareLoader(path: string, options: {ensurePreconnect?: boolean} = {
ensurePreconnect: true
}) {
if (ngDevMode && !isValidPath(path)) {
throwInvalidPathError(path);
}
path = normalizePath(path);
export const provideCloudflareLoader =
createImageLoader(cloudflareLoaderFactory, throwInvalidPathError);
const providers: Provider[] = [{
provide: IMAGE_LOADER,
useValue: (config: ImageLoaderConfig) => {
let params = `format=auto`;
if (config.width) {
params += `,width=${config.width.toString()}`;
}
const url = `${path}/cdn-cgi/image/${params}/${normalizeSrc(config.src)}`;
return url;
function cloudflareLoaderFactory(path: string) {
return (config: ImageLoaderConfig) => {
let params = `format=auto`;
if (config.width) {
params += `,width=${config.width.toString()}`;
}
}];
if (ngDevMode && Boolean(options.ensurePreconnect) === true) {
providers.push({provide: PRECONNECT_CHECK_BLOCKLIST, useValue: [path], multi: true});
}
return providers;
const url = `${path}/cdn-cgi/image/${params}/${normalizeSrc(config.src)}`;
return url;
};
}
function throwInvalidPathError(path: unknown): never {
@@ -6,13 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Provider, ɵRuntimeError as RuntimeError} from '@angular/core';
import {ɵRuntimeError as RuntimeError} from '@angular/core';
import {RuntimeErrorCode} from '../../../errors';
import {PRECONNECT_CHECK_BLOCKLIST} from '../preconnect_link_checker';
import {isValidPath, normalizePath, normalizeSrc} from '../util';
import {normalizeSrc} from '../util';
import {IMAGE_LOADER, ImageLoaderConfig} from './image_loader';
import {createImageLoader, ImageLoaderConfig} from './image_loader';
/**
* Function that generates a built-in ImageLoader for Cloudinary
@@ -29,37 +28,20 @@ import {IMAGE_LOADER, ImageLoaderConfig} from './image_loader';
* present in the document's `<head>`.
* @returns Set of providers to configure the Cloudinary loader.
*/
export function provideCloudinaryLoader(path: string, options: {ensurePreconnect?: boolean} = {
ensurePreconnect: true
}): Provider[] {
if (ngDevMode && !isValidPath(path)) {
throwInvalidPathError(path);
}
path = normalizePath(path);
export const provideCloudinaryLoader =
createImageLoader(cloudinaryLoaderFactory, throwInvalidPathError);
const providers: Provider[] = [{
provide: IMAGE_LOADER,
useValue: (config: ImageLoaderConfig) => {
// Example of a Cloudinary image URL:
// https://res.cloudinary.com/mysite/image/upload/c_scale,f_auto,q_auto,w_600/marketing/tile-topics-m.png
let params = `f_auto,q_auto`; // sets image format and quality to "auto"
if (config.width) {
params += `,w_${config.width}`;
}
const url = `${path}/image/upload/${params}/${normalizeSrc(config.src)}`;
return url;
function cloudinaryLoaderFactory(path: string) {
return (config: ImageLoaderConfig) => {
// Example of a Cloudinary image URL:
// https://res.cloudinary.com/mysite/image/upload/c_scale,f_auto,q_auto,w_600/marketing/tile-topics-m.png
let params = `f_auto,q_auto`; // sets image format and quality to "auto"
if (config.width) {
params += `,w_${config.width}`;
}
}];
if (ngDevMode && Boolean(options.ensurePreconnect) === true) {
providers.push({
provide: PRECONNECT_CHECK_BLOCKLIST,
useValue: [path],
multi: true,
});
}
return providers;
const url = `${path}/image/upload/${params}/${normalizeSrc(config.src)}`;
return url;
};
}
function throwInvalidPathError(path: unknown): never {
@@ -6,7 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/
import {InjectionToken} from '@angular/core';
import {InjectionToken, Provider} from '@angular/core';
import {PRECONNECT_CHECK_BLOCKLIST} from '../preconnect_link_checker';
import {isValidPath, normalizePath, normalizeSrc} from '../util';
/**
* Config options recognized by the image loader function.
@@ -37,3 +40,24 @@ export const IMAGE_LOADER = new InjectionToken<ImageLoader>('ImageLoader', {
providedIn: 'root',
factory: () => noopImageLoader,
});
export function createImageLoader(
urlFn: (path: string) => ImageLoader,
invalidPathFn: (path: unknown) => void,
) {
return function provideImageLoader(
path: string, options: {ensurePreconnect?: boolean} = {ensurePreconnect: true}) {
if (ngDevMode && !isValidPath(path)) {
invalidPathFn(path);
}
path = normalizePath(path);
const providers: Provider[] = [{provide: IMAGE_LOADER, useValue: urlFn(path)}];
if (ngDevMode && Boolean(options.ensurePreconnect) === true) {
providers.push({provide: PRECONNECT_CHECK_BLOCKLIST, useValue: [path], multi: true});
}
return providers;
};
}
@@ -6,13 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Provider, ɵRuntimeError as RuntimeError} from '@angular/core';
import {ɵRuntimeError as RuntimeError} from '@angular/core';
import {RuntimeErrorCode} from '../../../errors';
import {PRECONNECT_CHECK_BLOCKLIST} from '../preconnect_link_checker';
import {isValidPath, normalizePath, normalizeSrc} from '../util';
import {normalizeSrc} from '../util';
import {IMAGE_LOADER, ImageLoaderConfig} from './image_loader';
import {createImageLoader, ImageLoaderConfig} from './image_loader';
/**
* Function that generates a built-in ImageLoader for ImageKit
@@ -28,37 +27,20 @@ import {IMAGE_LOADER, ImageLoaderConfig} from './image_loader';
* present in the document's `<head>`.
* @returns Set of providers to configure the ImageKit loader.
*/
export function provideImageKitLoader(path: string, options: {ensurePreconnect?: boolean} = {
ensurePreconnect: true
}): Provider[] {
if (ngDevMode && !isValidPath(path)) {
throwInvalidPathError(path);
}
path = normalizePath(path);
export const provideImageKitLoader =
createImageLoader(imagekitLoaderFactory, throwInvalidPathError);
const providers: Provider[] = [{
provide: IMAGE_LOADER,
useValue: (config: ImageLoaderConfig) => {
// Example of an ImageKit image URL:
// https://ik.imagekit.io/demo/tr:w-300,h-300/medium_cafe_B1iTdD0C.jpg
let params = `tr:q-auto`; // applies the "auto quality" transformation
if (config.width) {
params += `,w-${config.width?.toString()}`;
}
const url = `${path}/${params}/${normalizeSrc(config.src)}`;
return url;
export function imagekitLoaderFactory(path: string) {
return (config: ImageLoaderConfig) => {
// Example of an ImageKit image URL:
// https://ik.imagekit.io/demo/tr:w-300,h-300/medium_cafe_B1iTdD0C.jpg
let params = `tr:q-auto`; // applies the "auto quality" transformation
if (config.width) {
params += `,w-${config.width?.toString()}`;
}
}];
if (ngDevMode && Boolean(options.ensurePreconnect) === true) {
providers.push({
provide: PRECONNECT_CHECK_BLOCKLIST,
useValue: [path],
multi: true,
});
}
return providers;
const url = `${path}/${params}/${normalizeSrc(config.src)}`;
return url;
};
}
function throwInvalidPathError(path: unknown): never {
@@ -6,13 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Provider, ɵRuntimeError as RuntimeError} from '@angular/core';
import {ɵRuntimeError as RuntimeError} from '@angular/core';
import {RuntimeErrorCode} from '../../../errors';
import {PRECONNECT_CHECK_BLOCKLIST} from '../preconnect_link_checker';
import {isValidPath, normalizePath, normalizeSrc} from '../util';
import {normalizeSrc} from '../util';
import {IMAGE_LOADER, ImageLoaderConfig} from './image_loader';
import {createImageLoader, ImageLoaderConfig} from './image_loader';
/**
* Function that generates a built-in ImageLoader for Imgix and turns it
@@ -26,34 +25,16 @@ import {IMAGE_LOADER, ImageLoaderConfig} from './image_loader';
* present in the document's `<head>`.
* @returns Set of providers to configure the Imgix loader.
*/
export function provideImgixLoader(path: string, options: {ensurePreconnect?: boolean} = {
ensurePreconnect: true
}): Provider[] {
if (ngDevMode && !isValidPath(path)) {
throwInvalidPathError(path);
}
path = normalizePath(path);
export const provideImgixLoader = createImageLoader(imgixLoaderFactory, throwInvalidPathError);
const providers: Provider[] = [{
provide: IMAGE_LOADER,
useValue: (config: ImageLoaderConfig) => {
const url = new URL(`${path}/${normalizeSrc(config.src)}`);
// This setting ensures the smallest allowable format is set.
url.searchParams.set('auto', 'format');
config.width && url.searchParams.set('w', config.width.toString());
return url.href;
}
}];
if (ngDevMode && Boolean(options.ensurePreconnect) === true) {
providers.push({
provide: PRECONNECT_CHECK_BLOCKLIST,
useValue: [path],
multi: true,
});
}
return providers;
function imgixLoaderFactory(path: string) {
return (config: ImageLoaderConfig) => {
const url = new URL(`${path}/${normalizeSrc(config.src)}`);
// This setting ensures the smallest allowable format is set.
url.searchParams.set('auto', 'format');
config.width && url.searchParams.set('w', config.width.toString());
return url.href;
};
}
function throwInvalidPathError(path: unknown): never {