@nacelle/compatibility-connector
Version:
Connect @nacelle/client-js-sdk to Nacelle's v2 back end with minimal code changes
245 lines (214 loc) • 5.91 kB
text/typescript
import type { Media as MediaV1, Maybe } from '@nacelle/types';
import type { Media as MediaV2 } from 'storefrontSdkV1';
import type { ContentfulEntry, SourceName } from '.';
export type ContentfulMedia = ContentfulEntry & {
fields: {
description?: string;
file: {
url: string;
details: object;
fileName: string;
contentType: string;
};
};
};
export interface SanityMedia {
_type: 'image' | 'video';
asset: {
_createdAt: string;
_id: string;
_rev: string;
_type: string;
_updatedAt: string;
_depth?: number;
assetId: string;
extension: string;
metadata?: {
_type: string;
palette?: Record<string, string | number>;
};
mimeType: string;
originalFilename: string;
path: string;
sha1hash: string;
size: number;
uploadId: string;
url: string;
};
}
export const isContentfulMedia = (x: unknown): x is ContentfulMedia =>
typeof (x as ContentfulMedia)?.sys === 'object' &&
(x as ContentfulMedia).sys.type === 'Asset';
export const isSanityMedia = (x: unknown): x is SanityMedia =>
typeof (x as SanityMedia)?._type === 'string' &&
typeof (x as SanityMedia)?.asset === 'object';
export function flattenContentfulMedia(entry: ContentfulMedia): MediaV2 {
const { file = { url: '', contentType: '' }, description = '' } =
entry.fields;
const { url = '', contentType = '' } = file;
const { id = '' } = entry.sys;
return {
id,
src: url,
thumbnailSrc: url,
type: contentType,
altText: description
};
}
export function flattenSanityMedia(media: SanityMedia): MediaV2 {
const {
asset: { assetId, mimeType, url },
_type
} = media;
let thumbnailSrc = url;
if (_type === 'image') {
// In the v1 Sanity connector, `?w=100` was added to `url` to create `thumbnailSrc`.
// No such transformation was made in the v1 Contentful connector.
try {
const imageUrl = new URL(url);
if (!imageUrl.searchParams.has('w')) {
imageUrl.searchParams.append('w', '100');
thumbnailSrc = imageUrl.toString();
}
} catch {
// Do nothing
}
}
return {
id: assetId,
src: url,
thumbnailSrc,
type: mimeType,
altText: ''
};
}
export interface UnformattedMedia {
[key: string]: string | null;
}
function getImageMimeType(extension: string): string {
// Extensions for which we will make an effort to infer a MIME type
const commonImageExtensions = [
'jpg',
'jpeg',
'png',
'gif',
'svg',
'webp',
'avif'
];
const isSupportedExtension = commonImageExtensions.indexOf(extension) !== -1;
if (!isSupportedExtension) {
return '';
}
switch (extension) {
case 'jpg': {
return 'image/jpeg';
}
case 'svg': {
return 'image/svg+xml';
}
default: {
// this works for all other commonImageExtensions
return 'image/' + extension;
}
}
}
function getVideoMimeType(extension: string): string {
// Extensions for which we will make an effort to infer a MIME type
const commonVideoExtensions = ['mp4', 'mov', 'mpeg', 'webm'];
const isSupportedExtension = commonVideoExtensions.indexOf(extension) !== -1;
if (!isSupportedExtension) {
return '';
}
if (extension === 'mov') {
return 'video/quicktime';
}
// this works for all non-'mov' commonVideoExtensions
return 'video/' + extension;
}
/**
* Reshapes CMS-sources media assets to a consistent format.
* @param mediaInput
* @returns - either original input if not Contentful or Sanity media, or a flattened version of the media that makes the necessary fields more top-level/readily available
*/
function formatMedia(
mediaInput: Maybe<MediaV2> | UnformattedMedia,
cms?: SourceName
): Maybe<MediaV2> | UnformattedMedia {
if (!cms || !mediaInput) {
return mediaInput;
}
if (cms === 'CONTENTFUL' && isContentfulMedia(mediaInput)) {
return flattenContentfulMedia(mediaInput) as MediaV2 | UnformattedMedia;
} else if (cms === 'SANITY' && isSanityMedia(mediaInput)) {
return flattenSanityMedia(mediaInput) as MediaV2 | UnformattedMedia;
} else {
return mediaInput;
}
}
export function transformMedia(
mediaInput: Maybe<MediaV2> | UnformattedMedia,
cms?: SourceName
): Maybe<MediaV1> {
if (
!mediaInput ||
typeof mediaInput !== 'object' ||
Array.isArray(mediaInput)
) {
return null;
}
const newMedia: MediaV1 = {
altText: null,
id: null,
src: '',
thumbnailSrc: '',
type: ''
};
// because CMSs have complex file structures for media, we need to flatten media objects before transforming them
const formattedMedia = (formatMedia(mediaInput, cms) as MediaV2) ?? {};
for (const [key, value] of Object.entries(formattedMedia)) {
if (value) {
switch (key) {
case 'id': {
if (typeof value !== 'object') {
newMedia.id = value;
}
break;
}
case 'type': {
if (typeof value === 'string') {
newMedia.type = value.toLowerCase();
}
break;
}
default: {
if (typeof value === 'string') {
newMedia[key as keyof MediaV1] = value;
}
}
}
}
}
if (!newMedia.thumbnailSrc) {
newMedia.thumbnailSrc = newMedia.src;
}
// Attempt to infer the MIME type
if (!newMedia.type) {
let extension = '';
try {
const mediaUrl = new URL(newMedia.src);
extension = mediaUrl.pathname.split('.').pop() as string;
} catch (e) {
// Do nothing
}
if (extension) {
const imageExtension = getImageMimeType(extension);
const videoExtension = getVideoMimeType(extension);
const mimeType = imageExtension || videoExtension;
if (mimeType) {
newMedia.type = mimeType;
}
}
}
return newMedia;
}