UNPKG

@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
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; }