import defu from 'defu' ; import { imageMeta } from 'image-meta' ; import { hasProtocol , joinURL , withLeadingSlash } from 'ufo' ; import type { Source , SourceData } from './types' ; import { createFilesystemSource , createHTTPSource } from './sources' ; import { applyHandler , getHandler } from './handlers' ; import { cachedPromise , getEnv , createError } from './utils' ; export interface ImageMeta { width : number ; height : number ; type : string ; mimeType : string ; } export interface IPXCTX { sources : Record < string , Source > ; } export type IPX = ( id : string , modifiers ? : Record < string , string > , reqOptions ? : any ) => { src : ( ) => Promise < SourceData > ; data : ( ) => Promise < { data : Buffer ; meta : ImageMeta ; format : string ; } > ; } ; export interface IPXOptions { dir ? : false | string ; domains ? : false | string [ ] ; alias : Record < string , string > ; sharp ? : { [ key ] : any ; } ; } const SUPPORTED_FORMATS = [ 'jpeg' , 'png' , 'webp' , 'avif' , 'tiff' ] ; export function createIPX ( userOptions ) { const defaults = { dir : getEnv ( 'IPX_DIR' , '.' ) , domains : getEnv ( 'IPX_DOMAINS' , [ ] ) , alias : getEnv ( 'IPX_ALIAS' , { } ) , sharp : { } } ; const options = defu ( userOptions , defaults ) as IPXOptions ; options . alias = Object . fromEntries ( Object . entries ( options . alias ) . map ( e => [ withLeadingSlash ( e [ 0 ] ) , e [ 1 ] ] ) ) ; const ctx = { sources : { } } ; if ( options . dir ) { ctx . sources . filesystem = createFilesystemSource ( { dir : options . dir } ) ; } if ( options . domains ) { ctx . sources . http = createHTTPSource ( { domains : options . domains } ) ; } return function ipx ( id , modifiers = { } , reqOptions = { } ) { if ( ! id ) { throw createError ( 'resource id is missing' , 400 ) ; } id = hasProtocol ( id ) ? id : withLeadingSlash ( id ) ; for ( const base in options . alias ) { if ( id . startsWith ( base ) ) { id = joinURL ( options . alias [ base ] , id . substr ( base . length ) ) ; } } const getSrc = cachedPromise ( ( ) => { const source = hasProtocol ( id ) ? 'http' : 'filesystem' ; if ( ! ctx . sources [ source ] ) { throw createError ( 'Unknown source: ' + source , 400 ) ; } return ctx . sources [ source ] ( id , reqOptions ) ; } ) ; const getData = cachedPromise ( async ( ) => { const src = await getSrc ( ) ; const data = await src . getData ( ) ; const meta = imageMeta ( data ) as ImageMeta ; const mFormat = modifiers . f || modifiers . format ; let format = mFormat || meta . type ; if ( format === 'jpg' ) { format = 'jpeg' ; } if ( meta . type === 'svg' && ! mFormat ) { return { data , format : 'svg+xml' , meta } ; } const animated = modifiers . animated !== undefined || modifiers . a !== undefined ; if ( animated ) { format = 'webp' ; } const Sharp = await import ( 'sharp' ) . then ( r => r . default || r ) as typeof import ( 'sharp' ) ; let sharp = Sharp ( data , { animated } ) ; Object . assign ( ( sharp as any ) . options , options . sharp ) ; const handlers = Object . entries ( modifiers ) . map ( ( [ name , args ] ) => ( { handler : getHandler ( name ) , name , args } ) ) . filter ( h => h . handler ) . sort ( ( a , b ) => { const aKey = ( ( a . handler . order || a . name || '' ) ) . toString ( ) ; const bKey = ( ( b . handler . order || b . name || '' ) ) . toString ( ) ; return aKey . localeCompare ( bKey ) ; } ) ; const handlerCtx = { meta } ; for ( const h of handlers ) { sharp = applyHandler ( handlerCtx , sharp , h . handler , h . args ) || sharp ; } if ( SUPPORTED_FORMATS . includes ( format ) ) { sharp = sharp . toFormat ( format as any , { quality : handlerCtx . quality , progressive : format === 'jpeg' } ) ; } const newData = await sharp . toBuffer ( ) ; return { data : newData , format , meta } ; } ) ; return { src : getSrc , data : getData } ; } ; }