import { ElementFound , FetchEnd , NetworkData , Parser } from 'hint/dist/src/lib/types' ; import { normalizeString } from '@hint/utils-string' ; import { isHTTP , isHTTPS } from '@hint/utils-network' ; import { ManifestEvents } from './types' ; import { Engine } from 'hint/dist/src/lib/engine' ; import { IJSONResult , parseJSON , SchemaValidationResult , validate } from '@hint/utils-json' ; export * from './types' ; const schema = require ( './schema.json' ) ; export default class ManifestParser extends Parser < ManifestEvents > { private readonly fetchEndEventName = 'fetch::end::manifest' ; private readonly fetchErrorEventName = 'fetch::error::manifest' ; private readonly fetchStartEventName = 'fetch::start::manifest' ; private readonly parseEndEventName = 'parse::end::manifest' ; private readonly parseErrorSchemaEventName = 'parse::error::manifest::schema' ; private readonly parseJSONErrorEventName = 'parse::error::manifest::json' ; private fetchedManifests = new Set < string > ( ) ; public constructor ( engine ) { super ( engine , 'manifest' ) ; engine . on ( 'element::link' , this . fetchManifest . bind ( this ) ) ; engine . on ( 'fetch::end::manifest' , this . validateManifest . bind ( this ) ) ; engine . on ( 'scan::end' , this . onScanEnd . bind ( this ) ) ; } private onScanEnd ( ) { this . fetchedManifests . clear ( ) ; } private async fetchManifest ( elementFound ) { const { element , resource } = elementFound ; if ( ! isHTTP ( resource ) && ! isHTTPS ( resource ) ) { return ; } if ( normalizeString ( element . getAttribute ( 'rel' ) ) !== 'manifest' ) { return ; } const hrefValue = normalizeString ( element . getAttribute ( 'href' ) ) ; if ( ! hrefValue ) { return ; } const manifestURL = element . resolveUrl ( hrefValue ) ; if ( this . fetchedManifests . has ( manifestURL ) ) { return ; } await this . engine . emitAsync ( this . fetchStartEventName , { resource } ) ; let manifestNetworkData ; try { manifestNetworkData = await this . engine . fetchContent ( manifestURL ) ; } catch ( error ) { await this . engine . emitAsync ( this . fetchErrorEventName , { element , error , hops : [ manifestURL ] , resource : manifestURL } ) ; return ; } if ( this . fetchedManifests . has ( manifestURL ) ) { return ; } await this . engine . emitAsync ( this . fetchEndEventName , { element , request : manifestNetworkData . request , resource : manifestURL , response : manifestNetworkData . response } ) ; } private async validateManifest ( fetchEnd ) { const { resource , response } = fetchEnd ; if ( this . fetchedManifests . has ( resource ) ) { return ; } this . fetchedManifests . add ( resource ) ; if ( response . statusCode >= 400 ) { return ; } await this . engine . emitAsync ( `parse::start::manifest` , { resource } ) ; let result ; try { result = parseJSON ( response . body . content ) ; } catch ( e ) { await this . engine . emitAsync ( this . parseJSONErrorEventName , { error : e , resource } ) ; return ; } const validationResult = validate ( schema , result . data , result . getLocation ) ; if ( ! validationResult . valid ) { await this . engine . emitAsync ( this . parseErrorSchemaEventName , { error : new Error ( 'Invalid manifest' ) , errors : validationResult . errors , groupedErrors : validationResult . groupedErrors , prettifiedErrors : validationResult . prettifiedErrors , resource } ) ; return ; } await this . engine . emitAsync ( this . parseEndEventName , { getLocation : result . getLocation , parsedContent : validationResult . data , resource } ) ; } }