import { ProtoFab , filenameOutsideFabLocations , getContentType } from '@fab/core' ; import { InlineAssets , RenamedAssets , RewireAssetsArgs , RewireAssetsMetadata , } from './types' ; import hasha from 'hasha' ; import path from 'path' ; import { InvalidConfigError , _log } from '@fab/cli' ; import { isBinaryPromise } from 'istextorbinary' ; import { DEFAULT_IMMUTABILITY_CHECK } from './constants' ; const log = _log ( '@fab/plugin-rewire-assets' ) ; export async function build ( args , proto_fab ) { const { 'inline-threshold' : inline_threshold = 8192 , 'chunk-threshold' : chunk_threshold = undefined , 'treat-as-immutable' : immutable_regexp = DEFAULT_IMMUTABILITY_CHECK , } = args ; if ( Number . isNaN ( inline_threshold ) ) { throw new InvalidConfigError ( `'inline-threshold' value must be a number!` ) ; } if ( ! ( typeof chunk_threshold === 'undefined' || ( Number . isSafeInteger ( chunk_threshold ) && chunk_threshold > 0 ) ) ) { throw new InvalidConfigError ( `'chunk-threshold' value must be a positive integer!` ) ; } if ( ! ( immutable_regexp instanceof RegExp ) ) { throw new InvalidConfigError ( `'treat-as-immutable' value must be a regex-parser compatible RegExp string!` ) ; } const to_inline = [ ] ; const to_rename = [ ] ; for ( const [ filename , contents ] of proto_fab . files . entries ( ) ) { if ( filenameOutsideFabLocations ( filename ) ) { if ( contents . byteLength > inline_threshold || ( await isBinaryPromise ( filename , contents ) ) ) { to_rename . push ( filename ) ; } else { to_inline . push ( filename ) ; } } } log ( `Inlining 💛${ to_inline . length }💛 asset${ to_inline . length === 1 ? '' : 's' }.` ) ; const inlined_assets = { } ; for ( const filename of to_inline ) { inlined_assets [ filename ] = { contents : proto_fab . files . get ( filename ) ! . toString ( 'utf8' ) , content_type : getContentType ( filename ) , immutable : ! ! immutable_regexp . exec ( filename ) , } ; proto_fab . files . delete ( filename ) ; } log . tick ( `Done.` ) ; log ( `Generating server code to rewire 💛${ to_rename . length }💛 asset${ to_rename . length === 1 ? '' : 's' }.` ) ; const renamed_assets = { } ; for ( const filename of to_rename ) { const contents = proto_fab . files . get ( filename ) ! ; const immutable = ! ! immutable_regexp . exec ( filename ) ; const fingerprinted_name = immutable ? filename : getFingerprintedName ( contents , filename ) ; const chunks = splitInChunks ( contents , `/_assets${ fingerprinted_name }` , chunk_threshold ) ; renamed_assets [ filename ] = { chunks_paths : chunks . map ( ( chunk ) => chunk . path ) , immutable , } ; for ( const chunk of chunks ) { proto_fab . files . set ( chunk . path , chunk . contents ) ; } proto_fab . files . delete ( filename ) ; } log . tick ( `Done.` ) ; proto_fab . metadata . rewire_assets = { inlined_assets , renamed_assets } ; } const getFingerprintedName = ( contents , filename ) => { const hash = hasha ( contents , { algorithm : 'md5' } ) . slice ( 0 , 9 ) ; const extname = path . extname ( filename ) ; return extname ? `${ filename . slice ( 0 , - 1 * extname . length ) }.${ hash }${ extname }` : `${ filename }_${ hash }` ; } ; interface Chunk { path : string ; contents : Buffer ; } function splitInChunks ( contents , filename , chunk_threshold ) { if ( ! chunk_threshold || contents . byteLength <= chunk_threshold ) { return [ { path : filename , contents } ] ; } else { const chunks = [ ] ; let remaining_number_of_bytes = contents . byteLength ; while ( remaining_number_of_bytes > 0 ) { const chunk_size = Math . min ( remaining_number_of_bytes , chunk_threshold ) ; chunks . push ( { path : `${ filename }~${ chunks . length + 1 }` , contents : Buffer . from ( contents . buffer , contents . byteOffset + chunks . length * chunk_threshold , chunk_size ) , } ) ; remaining_number_of_bytes -= chunk_size ; } return chunks ; } }