@sanity/asset-utils
Version:
<!-- This file is AUTO-GENERATED, edit _README.template.md or tweak scripts/generateDocs.ts -->
519 lines (471 loc) • 15.1 kB
text/typescript
import {
isAssetIdStub,
isAssetObjectStub,
isAssetPathStub,
isAssetUrlStub,
isCdnUrl,
isInProgressUpload,
isReference,
isSanityFileAsset,
isSanityImageAsset,
} from './asserters.js'
import {
cdnUrl,
dummyProject,
fileAssetFilenamePattern,
fileAssetType,
idPattern,
imageAssetFilenamePattern,
imageAssetType,
inProgressAssetAssetId,
inProgressAssetExtension,
inProgressAssetId,
pathPattern,
} from './constants.js'
import {UnresolvableError} from './errors.js'
import {getDefaultCrop, getDefaultHotspot} from './hotspotCrop.js'
import {parseFileAssetId, parseImageAssetId} from './parse.js'
import {
buildFilePath,
buildFileUrl,
buildImagePath,
buildImageUrl,
getUrlPath,
tryGetAssetPath,
} from './paths.js'
import type {
PathBuilderOptions,
ResolvedSanityFile,
ResolvedSanityImage,
SanityAssetSource,
SanityFileAsset,
SanityFileObjectStub,
SanityFileSource,
SanityImageAsset,
SanityImageDimensions,
SanityImageObjectStub,
SanityImageSource,
SanityProjectDetails,
} from './types.js'
import {getForgivingResolver} from './utils.js'
/**
* Returns the width, height and aspect ratio of a passed image asset, from any
* inferrable structure (id, url, path, asset document, image object etc)
*
* @param src - Input source (image object, asset, reference, id, url, path)
* @returns Object with width, height and aspect ratio properties
*
* @throws {@link UnresolvableError}
* Throws if passed image source could not be resolved to an asset ID
* @public
*/
export function getImageDimensions(src: SanityImageSource): SanityImageDimensions {
// Check if this is an in-progress upload
if (isInProgressUpload(src)) {
// Return placeholder dimensions for in-progress uploads
return {width: 0, height: 0, aspectRatio: 0}
}
const imageId = getAssetDocumentId(src)
const {width, height} = parseImageAssetId(imageId)
const aspectRatio = width / height
return {width, height, aspectRatio}
}
/**
* {@inheritDoc getImageDimensions}
* @returns Returns `undefined` instead of throwing if a value cannot be resolved
* @public
*/
export const tryGetImageDimensions = getForgivingResolver(getImageDimensions)
/**
* Returns the file extension for a given asset
*
* @param src - Input source (file/image object, asset, reference, id, url, path)
* @returns The file extension, if resolvable (no `.` included)
*
* @throws {@link UnresolvableError}
* Throws if passed asset source could not be resolved to an asset ID
* @public
*/
export function getExtension(src: SanityAssetSource): string {
// Check if this is an in-progress upload
if (isInProgressUpload(src)) {
// Return placeholder extension for in-progress uploads
return inProgressAssetExtension
}
return isFileSource(src)
? getFile(src, dummyProject).asset.extension
: getImage(src, dummyProject).asset.extension
}
/**
* {@inheritDoc getExtension}
* @returns Returns `undefined` instead of throwing if a value cannot be resolved
* @public
*/
export const tryGetExtension = getForgivingResolver(getExtension)
/**
* Tries to resolve an image object with as much information as possible,
* from any inferrable structure (id, url, path, image object etc)
*
* @param src - Input source (image object, asset, reference, id, url, path)
* @param project - Project ID and dataset the image belongs to
* @returns Image object
*
* @throws {@link UnresolvableError}
* Throws if passed image source could not be resolved to an asset ID
* @public
*/
export function getImage(
src: SanityImageSource,
project?: SanityProjectDetails,
): ResolvedSanityImage {
// Check if this is an in-progress upload
if (isInProgressUpload(src)) {
// Return a placeholder image object that allows rendering to continue
return {
asset: {
_id: inProgressAssetId,
_type: imageAssetType,
assetId: inProgressAssetAssetId,
extension: inProgressAssetExtension,
url: '',
path: '',
metadata: {
dimensions: {width: 1, height: 1, aspectRatio: 1},
},
},
crop: getDefaultCrop(),
hotspot: getDefaultHotspot(),
}
}
const projectDetails = project || tryGetProject(src)
const asset = getImageAsset(src, projectDetails)
const img = src as SanityImageObjectStub
return {
asset,
crop: img.crop || getDefaultCrop(),
hotspot: img.hotspot || getDefaultHotspot(),
}
}
/**
* {@inheritDoc getImage}
* @returns Returns `undefined` instead of throwing if a value cannot be resolved
* @public
*/
export const tryGetImage = getForgivingResolver(getImage)
/**
* Tries to resolve a (partial) image asset document with as much information as possible,
* from any inferrable structure (id, url, path, image object etc)
*
* @param src - Input source (image object, asset, reference, id, url, path)
* @param project - Project ID and dataset the image belongs to
* @returns Image asset document
*
* @throws {@link UnresolvableError}
* Throws if passed image source could not be resolved to an asset ID
* @public
*/
export function getImageAsset(
src: SanityImageSource,
project?: SanityProjectDetails,
): SanityImageAsset {
const projectDetails = project || getProject(src)
const pathOptions: PathBuilderOptions = {...projectDetails, useVanityName: false}
const _id = getAssetDocumentId(src)
const sourceObj = src as SanityImageObjectStub
const source = (sourceObj.asset || src) as SanityImageAsset
const metadata = source.metadata || {}
const {assetId, width, height, extension} = parseImageAssetId(_id)
const aspectRatio = width / height
const baseAsset: SanityImageAsset = {
...(isSanityImageAsset(src) ? src : {}),
_id,
_type: 'sanity.imageAsset',
assetId,
extension,
metadata: {
...metadata,
dimensions: {width, height, aspectRatio},
},
// Placeholders, overwritten below
url: '',
path: '',
}
return {
...baseAsset,
path: buildImagePath(baseAsset, pathOptions),
url: buildImageUrl(baseAsset, pathOptions),
}
}
/**
* {@inheritDoc getImageAsset}
* @returns Returns `undefined` instead of throwing if a value cannot be resolved
* @public
*/
export const tryGetImageAsset = getForgivingResolver(getImageAsset)
/**
* Tries to resolve an file object with as much information as possible,
* from any inferrable structure (id, url, path, file object etc)
*
* @param src - Input source (file object, asset, reference, id, url, path)
* @param project - Project ID and dataset the file belongs to
* @returns File object
*
* @throws {@link UnresolvableError}
* Throws if passed file source could not be resolved to an asset ID
* @public
*/
export function getFile(src: SanityFileSource, project?: SanityProjectDetails): ResolvedSanityFile {
// Check if this is an in-progress upload
if (isInProgressUpload(src)) {
// Return a placeholder file object that allows rendering to continue
return {
asset: {
_id: inProgressAssetId,
_type: fileAssetType,
assetId: inProgressAssetAssetId,
extension: inProgressAssetExtension,
url: '',
path: '',
metadata: {},
},
}
}
const projectDetails = project || tryGetProject(src)
const asset = getFileAsset(src, projectDetails)
return {asset}
}
/**
* {@inheritDoc getFile}
* @returns Returns `undefined` instead of throwing if a value cannot be resolved
* @public
*/
export const tryGetFile = getForgivingResolver(getFile)
/**
* Tries to resolve a (partial) file asset document with as much information as possible,
* from any inferrable structure (id, url, path, file object etc)
*
* @param src - Input source (file object, asset, reference, id, url, path)
* @param options - Project ID and dataset the file belongs to, along with other options
* @returns File asset document
*
* @throws {@link UnresolvableError}
* Throws if passed file source could not be resolved to an asset ID
* @public
*/
export function getFileAsset(src: SanityFileSource, options?: PathBuilderOptions): SanityFileAsset {
// Check if this is an in-progress upload
if (isInProgressUpload(src)) {
// Return a placeholder file object that allows rendering to continue
return {
assetId: inProgressAssetAssetId,
_id: inProgressAssetId,
_type: fileAssetType,
extension: inProgressAssetExtension,
metadata: {},
url: '',
path: '',
}
}
const projectDetails: PathBuilderOptions = {...(options || getProject(src)), useVanityName: false}
const _id = getAssetDocumentId(src)
const sourceObj = src as SanityFileObjectStub
const source = (sourceObj.asset || src) as SanityFileAsset
const {assetId, extension} = parseFileAssetId(_id)
const baseAsset: SanityFileAsset = {
...(isSanityFileAsset(src) ? src : {}),
_id,
_type: 'sanity.fileAsset',
assetId,
extension,
metadata: source.metadata || {},
// Placeholders, overwritten below
url: '',
path: '',
}
return {
...baseAsset,
path: buildFilePath(baseAsset, projectDetails),
url: buildFileUrl(baseAsset, projectDetails),
}
}
/**
* {@inheritDoc getFileAsset}
* @returns Returns `undefined` instead of throwing if a value cannot be resolved
* @public
*/
export const tryGetFileAsset = getForgivingResolver(getFileAsset)
/**
* Tries to resolve the asset document ID from any inferrable structure
*
* @param src - Input source (image/file object, asset, reference, id, url, path)
* @returns The asset document ID
*
* @throws {@link UnresolvableError}
* Throws if passed asset source could not be resolved to an asset document ID
* @public
*/
export function getAssetDocumentId(src: unknown): string {
// Check if this is an in-progress upload (has upload but no asset)
if (isInProgressUpload(src)) {
// Return a placeholder ID that indicates in-progress state
// This allows the render cycle to continue until asset is available
return inProgressAssetId
}
const source = isAssetObjectStub(src) ? src.asset : src
let id = ''
if (typeof source === 'string') {
id = getIdFromString(source)
} else if (isReference(source)) {
id = source._ref
} else if (isAssetIdStub(source)) {
id = source._id
} else if (isAssetPathStub(source)) {
id = idFromUrl(`${cdnUrl}/${source.path}`)
} else if (isAssetUrlStub(source)) {
id = idFromUrl(source.url)
}
const hasId = id && idPattern.test(id)
if (!hasId) {
throw new UnresolvableError(src)
}
return id
}
/**
* {@inheritDoc getAssetDocumentId}
* @returns Returns `undefined` instead of throwing if a value cannot be resolved
* @public
*/
export const tryGetAssetDocumentId = getForgivingResolver(getAssetDocumentId)
/**
* Tries to cooerce a string (ID, URL or path) to an image asset ID
*
* @param str - Input string (ID, URL or path)
* @returns string
*
*
* @throws {@link UnresolvableError}
* Throws if passed image source could not be resolved to an asset ID
* @public
*/
export function getIdFromString(str: string): string {
if (idPattern.test(str)) {
// Already an ID
return str
}
const isAbsoluteUrl = isCdnUrl(str)
const path = isAbsoluteUrl ? new URL(str).pathname : str
if (path.indexOf('/images') === 0 || path.indexOf('/files') === 0) {
// Full URL
return idFromUrl(str)
}
if (pathPattern.test(str)) {
// Path
return idFromUrl(`${cdnUrl}/${str}`)
}
if (isFileAssetFilename(str)) {
// Just a filename (projectId/dataset irrelevant: just need asset ID)
return idFromUrl(`${cdnUrl}/files/a/b/${str}`)
}
if (isImageAssetFilename(str)) {
// Just a filename (projectId/dataset irrelevant: just need asset ID)
return idFromUrl(`${cdnUrl}/images/a/b/${str}`)
}
throw new UnresolvableError(str)
}
/**
* {@inheritDoc getIdFromString}
* @returns Returns `undefined` instead of throwing if a value cannot be resolved
* @public
*/
export const tryGetIdFromString = getForgivingResolver(getIdFromString)
/**
* Converts from a full asset URL to just the asset document ID
*
* @param url - A full asset URL to convert
* @returns string
* @public
*/
function idFromUrl(url: string): string {
const path = getUrlPath(url)
const [type, , , fileName] = path.split('/')
const prefix = type.replace(/s$/, '')
return `${prefix}-${fileName.replace(/\./g, '-')}`
}
/**
* Resolves project ID and dataset the image belongs to, based on full URL or path
* @param src - Image URL or path
* @returns object | undefined
*
* @throws {@link UnresolvableError}
* Throws if passed image source could not be resolved to an asset ID
* @public
*/
export function getProject(src: SanityImageSource): SanityProjectDetails {
const path = tryGetAssetPath(src)
if (!path) {
throw new UnresolvableError(src, 'Failed to resolve project ID and dataset from source')
}
const [, , projectId, dataset] = path.match(pathPattern) || []
if (!projectId || !dataset) {
throw new UnresolvableError(src, 'Failed to resolve project ID and dataset from source')
}
return {projectId, dataset}
}
/**
* {@inheritDoc getProject}
* @returns Returns `undefined` instead of throwing if a value cannot be resolved
* @public
*/
export const tryGetProject = getForgivingResolver(getProject)
/**
* Returns whether or not the passed filename is a valid image asset filename
*
* @param filename - Filename to validate
* @returns Whether or not the filename is an image asset filename
* @public
*/
export function isImageAssetFilename(filename: string): boolean {
return imageAssetFilenamePattern.test(filename)
}
/**
* Returns whether or not the passed filename is a valid file asset filename
*
* @param filename - Filename to validate
* @returns Whether or not the filename is a file asset filename
* @public
*/
export function isFileAssetFilename(filename: string): boolean {
return fileAssetFilenamePattern.test(filename)
}
/**
* Returns whether or not the passed filename is a valid file or image asset filename
*
* @param filename - Filename to validate
* @returns Whether or not the filename is an asset filename
* @public
*/
export function isAssetFilename(filename: string): boolean {
return isImageAssetFilename(filename) || isFileAssetFilename(filename)
}
/**
* Return whether or not the passed source is a file source
*
* @param src - Source to check
* @returns Whether or not the given source is a file source
* @public
*/
export function isFileSource(src: unknown): src is SanityFileSource {
const assetId = tryGetAssetDocumentId(src)
return assetId ? assetId.startsWith('file-') : false
}
/**
* Return whether or not the passed source is an image source
*
* @param src - Source to check
* @returns Whether or not the given source is an image source
* @public
*/
export function isImageSource(src: unknown): src is SanityImageSource {
const assetId = tryGetAssetDocumentId(src)
return assetId ? assetId.startsWith('image-') : false
}