import { env } from '../utils/environment' ; import { AppEvents , Manifest , ManifestContext , ManifestDetectionResult } from '../utils/interfaces' ; import { cleanUrl } from '../utils/url' ; import { getManifestContext , getURL , setManifestContext , setURL , isManifestEdited } from './app-info' ; export const emitter = new EventTarget ( ) ; export let initialManifest ; export let emptyManifest = { dir : 'auto' , display : 'fullscreen' , name : 'placeholder' , short_name : 'placeholder' , start_url : undefined , scope : '/' , lang : 'en' , description : 'placeholder description' , theme_color : 'none' , background_color : 'none' , icons : [ ] , screenshots : [ ] , } ; async function getManifestViaPuppeteer ( url ) { const encodedUrl = encodeURIComponent ( url ) ; const manifestTestUrl = `${ env . api }/WebManifest?site=${ encodedUrl }` ; const response = await fetch ( manifestTestUrl , { method : 'POST' , } ) ; if ( ! response . ok ) { console . warn ( 'Fetching manifest via Puppeteer service failed' , response . statusText ) ; throw new Error ( `Unable to fetch response using ${ manifestTestUrl }. Response status  ${ response }` ) ; } const responseData = await response . json < PuppeteerManifestFinderResult > ( ) ; if ( ! responseData ) { console . warn ( 'Fetching manifest via Puppeteer failed due to no response data' , response ) ; throw new Error ( `Unable to get JSON from ${ manifestTestUrl }` ) ; } if ( ! responseData . content || ! responseData . content . json ) { console . info ( "Manifest detection via Puppeteer completed, but couldn't detect the manifest." , responseData ) ; throw new Error ( "HTML parse manifest detector couldn't find the manifest. " + responseData . error ) ; } return { content : responseData . content . json , format : 'w3c' , generatedUrl : responseData . url || url , siteUrl : url , default : { short_name : responseData . content . json . short_name || '' , } , id : '' , generated : responseData . content ? false : true , errors : [ ] , suggestions : [ ] , warnings : [ ] , } ; } async function getManifestViaHtmlParse ( url ) { const manifestTestUrl = `${ env . manifestFinderUrl }?url=${ encodeURIComponent ( url ) }` ; const response = await fetch ( manifestTestUrl ) ; if ( ! response . ok ) { console . warn ( 'Fetching manifest via HTML parsing service failed' , response ) ; throw new Error ( `Error fetching from ${ manifestTestUrl }` ) ; } const responseData = await response . json < HtmlParseManifestFinderResult > ( ) ; if ( responseData . error || ! responseData . manifestContents ) { console . warn ( 'Fetching manifest via HTML parsing service failed due to no response data' , response ) ; throw new Error ( responseData . error || "Manifest couldn't be fetched" ) ; } if ( ! responseData . manifestContents ) { console . info ( "Manifest detection via HTML parse completed, but couldn't detect the manifest." , responseData ) ; throw new Error ( "Manifest detection via HTML parsing completed but couldn't find the manifest. " + responseData . error ) ; } return { content : responseData . manifestContents , format : 'w3c' , generatedUrl : responseData . manifestUrl || url , siteUrl : url , default : { short_name : responseData . manifestContents . short_name || '' , } , id : '' , generated : responseData . manifestContents ? false : true , errors : responseData . error ? [ responseData . error ] : [ ] , suggestions : [ ] , warnings : Object . entries ( responseData . warnings ) . map ( keyVal => `${ keyVal [ 0 ] }: ${ keyVal [ 1 ] }` ) , manifestContainsInvalidJson : responseData . manifestContainsInvalidJson , } ; } function timeoutAfter ( milliseconds ) { return new Promise < void > ( resolve => { setTimeout ( ( ) => resolve ( ) , milliseconds ) ; } ) ; } param url returns async function fetchManifest ( url ) { return new Promise ( async ( resolve , reject ) => { let knownGoodUrl ; try { knownGoodUrl = cleanUrl ( url ) ; } catch ( err ) { reject ( err ) ; return ; } const manifestDetectors = [ getManifestViaPuppeteer ( knownGoodUrl ) , getManifestViaHtmlParse ( knownGoodUrl ) , timeoutAfter ( 10000 ) ] ; const manifestDetectionResult = await Promise . any ( manifestDetectors ) ; if ( manifestDetectionResult ) { const context = getManifestContext ( ) ; if ( ! context . initialManifest ) { initialManifest = manifestDetectionResult . content ; context . initialManifest = initialManifest ; setManifestContext ( context ) ; } resolve ( manifestDetectionResult ) ; } else { console . error ( 'All manifest detectors failed: Timeout expired.' ) ; const createdManifest = await createManifestFromPageOrEmpty ( knownGoodUrl ) ; const createdManifestResult = wrapManifestInDetectionResult ( createdManifest , knownGoodUrl , true ) ; resolve ( createdManifestResult ) ; } } ) ; } param url returns export async function fetchOrCreateManifest ( url ? ) { const siteUrl = url || getSiteUrlFromManifestOrQueryString ( ) ; if ( ! siteUrl ) { throw new Error ( "No available site URL" ) ; } setURL ( siteUrl ) ; const detectionResult = await fetchManifest ( siteUrl ) ; const context = { manifest : detectionResult . content , initialManifest : initialManifest , manifestUrl : detectionResult . generatedUrl , isGenerated : detectionResult . generated , siteUrl : detectionResult . siteUrl , isEdited : false } ; setManifestContext ( context ) ; await updateManifest ( { ... detectionResult . content , } ) ; return context ; } returns function getSiteUrlFromManifestOrQueryString ( ) { const context = getManifestContext ( ) ; if ( context . siteUrl ) { return context . siteUrl ; } const search = new URLSearchParams ( location . search ) ; const siteQueryParam = search . get ( 'site' ) ; if ( siteQueryParam ) { return siteQueryParam ; } const sessionStorageUrl = getURL ( ) ; if ( sessionStorage ) { return sessionStorageUrl ; } return null ; } async function createManifestFromPageOrEmpty ( url ) { try { const response = await fetch ( `${ env . manifestCreatorUrl }?url=${ url }` , { method : 'POST' , headers : new Headers ( { 'content-type' : 'application/json' } ) , } ) ; const createdManifest = await response . json < Manifest > ( ) ; return createdManifest ; } catch ( err ) { console . error ( `Manifest creation service failed to create the manifest. Falling back to empty manifest.` , err ) ; return emptyManifest ; } } export function updateManifest ( manifestUpdates ) { const context = getManifestContext ( ) ; context . manifest = Object . assign ( context . manifest , manifestUpdates as Partial < Manifest > ) ; if ( context . initialManifest ) { isManifestEdited ( context . initialManifest , context . manifest ) ; } setManifestContext ( context ) ; emitter . dispatchEvent ( updateManifestEvent ( { ... context . manifest , } ) ) ; return context . manifest ; } export function updateManifestEvent < T extends Partial < Manifest > > ( detail ) { return new CustomEvent < T > ( AppEvents . manifestUpdate , { detail , bubbles : true , composed : true , } ) ; } async function wrapManifestInDetectionResult ( manifest , url , generated ) { return { content : manifest , format : "w3c" , siteUrl : url , generated : generated , id : "" , generatedUrl : "" , default : { short_name : manifest . short_name || "My PWA" } , errors : [ ] , suggestions : [ ] , warnings : [ ] } ; } type HtmlParseManifestFinderResult = { manifestUrl : string | null ; manifestContents : Manifest | null ; error : string | null ; manifestContainsInvalidJson : boolean ; manifestScore : { [ key in keyof Manifest | "manifest" ] : number ; } ; warnings : { [ key in keyof Manifest | string ] : string ; } ; } ; type PuppeteerManifestFinderResult = { content : { json : Manifest ; } | null ; url : string | null ; error ? : any ; } ;