"use strict" ;
const indentString = require ( "indent-string" ) ;
const matchVirtualProperty = require ( "@validatem/match-virtual-property" ) ;
const asExpression = require ( "as-expression" ) ;
const syncpipe = require ( "syncpipe" ) ;
const AggregrateValidationError = require ( "./aggregrate-validation-error" ) ;
const parseStacktrace = require ( "./parse-stacktrace" ) ;
const { dim , dimBold , highlight , highlightBold } = require ( "./colors" ) ;
// TODO: Omit the "At (root)" for path-less errors, to avoid confusion when singular values are being compared?
// TODO: Move out the path generating logic into a separate module, to better support custom error formatting code
// FIXME: Remove duplicate subError-less error messages, when heuristics are enabled? eg. multiple "Must be an array" for different arrayOf combinators
function joinPathSegments ( segments ) {
return ( segments . length > 0 )
? segments . join ( " -> " )
: "(root)" ;
}
// FIXME: Render error codes (grayed out) after error messages, as a stable identifier
function renderErrorList ( errors , subErrorLevels = 0 ) {
let rephrasedErrors = errors . map ( ( error , i ) => {
let pathSegments = error . path . map ( ( segment ) => {
if ( segment == null ) {
throw new Error ( ` Unexpected empty path segment encountered; this is a bug, please report it! ` ) ;
} else if ( typeof segment === "string" || typeof segment === "number" ) {
return highlight ( String ( segment ) ) ;
} else if ( matchVirtualProperty ( segment ) ) {
return dim ( ` ( ${ segment . name } ) ` ) ;
} else {
throw new Error ( ` Unexpected path segment encountered: ${ segment } ; this is a bug, please report it! ` ) ;
}
} ) ;
let lineCharacter = ( i < errors . length - 1 )
? "├─"
: "└─" ;
let mainLine = asExpression ( ( ) => {
if ( subErrorLevels > 0 ) {
let message = ( pathSegments . length > 0 )
? ` ${ lineCharacter } ${ joinPathSegments ( pathSegments ) } : ${ error . message } `
: ` ${ lineCharacter } ${ error . message } ` ;
return message ;
} else {
return ( pathSegments . length > 0 )
? ` - At ${ joinPathSegments ( pathSegments ) } : ${ error . message } `
: ` - ${ error . message } ` ;
}
} ) ;
if ( error . subErrors != null && error . subErrors . length > 0 ) {
let renderedSubErrors = renderErrorList ( error . subErrors , subErrorLevels + 1 ) ;
let isLastError = ( i === errors . length - 1 ) ;
if ( subErrorLevels > 0 && ! isLastError ) {
return syncpipe ( renderedSubErrors , [
( _ ) => indentString ( _ , 3 ) ,
( _ ) => indentString ( _ , 1 , { indent : "│" } ) ,
( _ ) => mainLine + "\n" + _
] ) ;
} else {
return mainLine + "\n" + indentString ( renderedSubErrors , 4 ) ;
}
} else {
return mainLine ;
}
} ) ;
return rephrasedErrors . map ( ( error ) => {
return ` ${ error } ` ;
} ) . join ( "\n" ) ;
}
function determineLocation ( ) {
try {
throw new Error ( ` Dummy error to obtain a stacktrace ` ) ;
} catch ( error ) {
try {
let externalFrames = syncpipe ( error , [
( _ ) => parseStacktrace ( _ ) ,
( _ ) => removeInternalFrames ( _ ) ,
] ) ;
if ( externalFrames . length > 0 ) {
return syncpipe ( externalFrames , [
( _ ) => _ [ 0 ] ,
( _ ) => {
return {
... _ ,
shortPath : abbreviatePath ( _ . location . path )
} ;
}
] ) ;
} else {
return null ;
}
} catch ( parsingError ) {
// If *anything at all* went wrong, we will just give up and return nothing, because the stacktrace parsing code is fragile, and we don't want that to be a reason for someone not to get a validation error displayed to them.
// FIXME: Do we want to have this visible as a warning in 1.0.0? Or should this warning be opt-in, for when the user wants more detail about *why* the location of the error could not be determined? Since there may be legitimate reasons for that to occur, eg. in bundled code without source maps.
console . warn ( "An error occurred during stacktrace parsing; please report this as a bug in @validatem/core!" , parsingError ) ;
}
}
}
// NOTE: This must be changed if aggregrate-errors.js is ever moved within the module!
let internalBasePathRegex = /(.+)src$/ ;
function getInternalBasePath ( ) {
let match = internalBasePathRegex . exec ( _ _dirname ) ;
if ( match != null ) {
return match [ 1 ] ;
} else {
throw new Error ( ` Did not find expected basePath, instead got: ${ _ _dirname } ` ) ;
}
}
function removeInternalFrames ( stack ) {
let internalBasePath = getInternalBasePath ( ) ;
if ( stack [ 0 ] . location != null && stack [ 0 ] . location . path != null && stack [ 0 ] . location . path . startsWith ( internalBasePath ) ) {
// We are running a normal environment with sensible stacktraces.
return stack . filter ( ( frame ) => {
return (
! frame . location . anonymous
&& ! frame . location . path . startsWith ( internalBasePath )
) ;
} ) ;
} else {
// We're probably in a bundled environment, with errors not being sourcemapped. Use an alternate, less reliable strategy. This will still break when code is minified.
let lastValidationFrame = stack . findIndex ( ( frame ) => {
return ( frame . functionName != null && frame . functionName . includes ( "createValidationMethod" ) ) ;
} ) ;
if ( lastValidationFrame === - 1 ) {
// Welp, this didn't work either. We'll just return an empty stack then, treating every frame as (possibly) internal, to cause the origin to be displayed as unknown.
return [ ] ;
} else {
return stack . slice ( lastValidationFrame + 1 ) ;
}
}
}
function abbreviatePath ( path ) {
// TODO: Maybe add a special case for paths within node_modules? For when an error originates from a package the user is depending on.
let segments = path . split ( /[\\\/]/ ) ;
let [ thirdLast , secondLast , last ] = segments . slice ( - 3 ) ;
if ( last != null ) {
let isIndexFile = /^index\.[a-z0-9]+$/ . test ( last ) ;
let relevantSegments = ( isIndexFile )
? [ thirdLast , secondLast , last ]
: [ secondLast , last ] ;
return relevantSegments . join ( "/" ) ;
} else {
// This path is extremely short, so we'll just return it as-is.
return segments . join ( "/" ) ;
}
}
module . exports = function aggregrateAndThrowErrors ( errors ) {
if ( errors . length > 0 ) {
let detailLines = renderErrorList ( errors ) ;
let frame = determineLocation ( ) ;
let locationString = asExpression ( ( ) => {
if ( frame != null ) {
let functionString = asExpression ( ( ) => {
if ( frame . alias != null && frame . functionName != null ) {
return ` ${ frame . alias } [ ${ frame . functionName } ] ` ;
} else if ( frame . functionName != null ) {
return ` ' ${ frame . functionName } ' ` ;
} else {
return dimBold ( "(unnamed function)" ) ;
}
} ) ;
return ` ${ highlightBold ( functionString ) } in ${ highlightBold ( frame . shortPath ) } , at line ${ highlightBold ( frame . location . line ) } ( ${ frame . location . path } ) ` ;
} else {
return dimBold ( ` (could not determine location) ` ) ;
}
} ) ;
return new AggregrateValidationError ( ` One or more validation errors occurred at ${ locationString } : \n ${ detailLines } ` , {
errors : errors
} ) ;
}
} ;