import { readFileSync } from 'fs' ; import * as path from 'path' ; import * as _ from 'lodash' ; import 'lodash.product' ; import * as RJSON from 'relaxed-json' ; import { ComponentPath } from './componentPath' ; export type ComponentAst = ReadonlyArray < ComponentTag > ; export interface InputMap { [ name ] : string ; } export interface ComponentTag { readonly fqtag : string ; readonly dvOf ? : string ; readonly dvAlias ? : string ; readonly tag : string ; readonly inputs ? : InputMap ; readonly context ? : InputMap ; readonly content ? : ComponentAst ; } export type ComponentTagPath = ComponentTag [ ] ; /**
 * The component table is indexed by tag. If in the content of two components we
 * have a component of the same tag (but different `of`, or different `alias`)
 * there would still be only one entry for that component in the table.
 */ export interface ComponentTable { readonly [ tag ] : ComponentAst ; } export interface FieldMap { [ field ] : string ; } export interface ComponentInput { tag : string ; dvOf ? : string ; dvAlias ? : string ; inputMap ? : FieldMap ; inputs ? : FieldMap ; } const COMPONENT_TABLE_FILE_NAME = 'componentTable.json' ; const CONFIG_FILE_NAME = 'dvconfig.json' ; const DV_CORE_CONCEPT = 'core' ; const INDENT_NUM_SPACES = 2 ; export class ComponentHelper { private readonly componentTable : ComponentTable ; private readonly componentsNoExecRequest : Set < string > ; private readonly componentsNoEvalRequest : Set < string > ; private readonly componentsRequestOptional : Set < string > ; private readonly noApp : boolean = false ; returns private static GetFqTag ( tag , dvOf , dvAlias ) { if ( dvAlias ) { return dvAlias ; } let [ conceptName , ... componentTagName ] = tag . split ( '-' ) ; if ( dvOf ) { conceptName = dvOf ; } return conceptName + '-' + componentTagName . join ( '-' ) ; } /**
     * Attempts to parse a component expression
     */ private static ParseComponentExpr ( expr ) { const errMsg = ( invalidExpr ) => `Expected component object or a variable but found ${ invalidExpr }.` + `(For an object to be a component object it must have a 'tag' field)` ; let componentExpr ; try { componentExpr = RJSON . parse ( expr ) ; } catch ( e ) { throw new Error ( errMsg ( expr ) ) ; } const isVariable = _ . isString ( componentExpr ) && ! _ . has ( componentExpr , ' ' ) ; if ( ! this . IsComponentInput ( componentExpr ) && ! isVariable ) { throw new Error ( errMsg ( expr ) ) ; } return componentExpr ; } private static IsComponentInput ( componentExpr ) { return _ . isPlainObject ( componentExpr ) && _ . has ( componentExpr , 'tag' ) ; } /**
     * Retrieves a component input object from the given component expression or
     * defaultSpec depending on the value of expr
     */ private static GetComponentInput ( componentExpr , defaultSpec ) { let componentInput ; if ( this . IsComponentInput ( componentExpr ) ) { componentInput = componentExpr ; } else { if ( _ . has ( defaultSpec , `no-default-${ componentExpr }` ) ) { return null ; } const defaultComponentInput = this . ParseComponentExpr ( _ . get ( defaultSpec , `default-${ componentExpr }` ) ) ; if ( ! this . IsComponentInput ( defaultComponentInput ) ) { throw new Error ( `No default hint given for value ${ componentExpr }` + `To give a default hint, set default-${ componentExpr }="{ tag: ... }"` ) ; } else { componentInput = defaultComponentInput ; } } return componentInput ; } param includeComponentTag returns throws private static GetIncludedComponentTag ( includeComponentTag ) { const noComponentErrorMsg = ( cause ) => `
      Couldn't find the included component in
      ${ JSON . stringify ( includeComponentTag ) }:
      ${ cause } \n Context is ${ JSON . stringify ( includeComponentTag . context ) }
    ` ; const unparsedComponentExpr = _ . get ( includeComponentTag . inputs , '[component]' ) ; if ( _ . isEmpty ( unparsedComponentExpr ) ) { throw new Error ( noComponentErrorMsg ( `No "component" input` ) ) ; } let componentInput ; try { const componentExpr = this . ParseComponentExpr ( unparsedComponentExpr as string ) ; if ( this . IsComponentInput ( componentExpr ) ) { componentInput = componentExpr ; } else { if ( _ . has ( includeComponentTag . context , `[${ componentExpr }]` ) ) { const unparsedParentComponentExpr = _ . get ( includeComponentTag . context , `[${ componentExpr }]` ) ; const parentComponentExpr = this . ParseComponentExpr ( unparsedParentComponentExpr as string ) ; componentInput = this . GetComponentInput ( parentComponentExpr , includeComponentTag . context ) ; } else { componentInput = this . GetComponentInput ( componentExpr , _ . get ( includeComponentTag , 'inputs' ) ) ; } } } catch ( e ) { e . message = noComponentErrorMsg ( e . message ) ; throw e ; } if ( componentInput === null ) { return null ; } const fqtag = ComponentHelper . GetFqTag ( componentInput . tag , componentInput . dvOf , componentInput . dvAlias ) ; const componentInputs = < InputMap > _ . get ( componentInput , 'inputs' , { } ) ; const inputs = _ . assign ( { } , _ . mapValues ( _ . invert ( < InputMap > componentInput . inputMap ) , ( value ) => { return _ . get ( includeComponentTag . context , value ) ; } ) , componentInputs ) ; return { fqtag : fqtag , tag : componentInput . tag , dvOf : componentInput . dvOf , dvAlias : componentInput . dvAlias , inputs : inputs , context : { } } ; } return private static GetComponentTableOfConcept ( concept ) { const fp = path . join ( ComponentHelper . GetConceptFolder ( concept ) , COMPONENT_TABLE_FILE_NAME ) ; return JSON . parse ( readFileSync ( fp , 'utf8' ) ) ; } return private static GetComponentsNoRequest ( concept ) { const fp = path . join ( ComponentHelper . GetConceptFolder ( concept ) , CONFIG_FILE_NAME ) ; return JSON . parse ( readFileSync ( fp , 'utf8' ) ) . componentsNoRequest ; } return private static GetComponentsRequestOptional ( concept ) { const fp = path . join ( ComponentHelper . GetConceptFolder ( concept ) , CONFIG_FILE_NAME ) ; return JSON . parse ( readFileSync ( fp , 'utf8' ) ) . componentsRequestOptional ; } private static GetConceptFolder ( concept ) { return path . join ( path . dirname ( require . resolve ( `@deja-vu/${ concept }` ) ) , '..' ) ; } returns private static ConceptOfTag ( tag ) { return tag . split ( '-' ) [ 0 ] ; } private static GetDvOfForChild ( componentTag , childComponentTag ) { if ( ! _ . isEmpty ( componentTag . dvOf ) && _ . isEmpty ( childComponentTag . dvOf ) && ComponentHelper . ConceptOfTag ( componentTag . tag ) === ComponentHelper . ConceptOfTag ( childComponentTag . tag ) ) { return componentTag . dvOf ; } return childComponentTag . dvOf ; } returns private static IsDvIncludeComponent ( component ) { return component . tag === 'dv-include' ; } returns private static IsDvTxComponent ( component ) { return component . tag === 'dv-tx' ; } private static ComponentExistsOrFail ( component , componentTable ) { if ( component . tag === 'router-outlet' ) { return ; } if ( component . tag . split ( '-' ) . length === 1 ) { return ; } if ( ! _ . has ( componentTable , component . tag ) ) { const errMsg = `Component ${ component . tag } doesn't exist in component ` + `table with keys ${ JSON . stringify ( _ . keys ( componentTable ) , null , INDENT_NUM_SPACES ) }` ; throw new Error ( errMsg ) ; } } static PickComponentTagPath ( componentPath , pickProps ) { return _ . map ( componentPath , ( componentTag ) => { const newContent = ComponentHelper . PickComponentTagPath ( componentTag . content , pickProps ) ; const rest = _ . pick ( componentTag , pickProps ) ; return _ . isEmpty ( newContent ) ? rest : { ... rest , content : newContent } ; } ) ; } param usedConcepts param appComponentTable param routeComponentSelectors constructor ( usedConcepts ? , appComponentTable ? , private readonly routeComponentSelectors ? ) { const conceptComponentTables = _ . map ( _ . uniq ( usedConcepts ) , ComponentHelper . GetComponentTableOfConcept ) ; const dvCoreComponentTable = ComponentHelper . GetComponentTableOfConcept ( DV_CORE_CONCEPT ) ; this . componentTable = _ . assign ( { } , appComponentTable || { } , ... conceptComponentTables , dvCoreComponentTable ) ; this . componentsNoExecRequest = new Set < string > ( _ . flatMap ( usedConcepts , ( concept ) => _ . get ( ComponentHelper . GetComponentsNoRequest ( concept ) , 'exec' , [ ] ) ) ) ; this . componentsNoEvalRequest = new Set < string > ( _ . flatMap ( usedConcepts , ( concept ) => _ . get ( ComponentHelper . GetComponentsNoRequest ( concept ) , 'eval' , [ ] ) ) ) ; this . componentsRequestOptional = new Set < string > ( _ . flatMap ( usedConcepts , ( concept ) : string [ ] => ComponentHelper . GetComponentsRequestOptional ( concept ) || [ ] ) ) ; if ( ! appComponentTable ) { this . noApp = true ; return ; } const usedComponents = new Set < string > ( _ . keys ( appComponentTable ) ) ; const saveUsedComponents = ( componentAst , debugPath ) : void => { _ . each ( componentAst , ( component ) => { const thisDebugPath = new Set ( debugPath ) ; if ( thisDebugPath . has ( component . fqtag ) ) { return ; } thisDebugPath . add ( component . fqtag ) ; usedComponents . add ( component . tag ) ; try { const componentContent = this . getContent ( component ) ; saveUsedComponents ( componentContent , thisDebugPath ) ; } catch ( e ) { if ( ! _ . has ( e , 'componentPath' ) ) { e . componentPath = [ ... thisDebugPath ] ; } throw e ; } } ) ; } ; try { _ . each ( _ . keys ( appComponentTable ) , ( tag ) => { const content = this . getContent ( { fqtag : tag , tag : tag } ) ; saveUsedComponents ( content , new Set ( [ tag ] ) ) ; } ) ; } catch ( e ) { e . message = `Error at path: ${ e . componentPath }\n${ e . message }` ; throw e ; } this . componentTable = _ . pick ( this . componentTable , Array . from ( usedComponents ) ) ; } returns shouldHaveExecRequest ( tag ) { return ! this . componentsNoExecRequest . has ( tag ) ; } returns shouldHaveEvalRequest ( tag ) { return ! this . componentsNoEvalRequest . has ( tag ) ; } isRequestOptional ( tag ) { return this . componentsRequestOptional . has ( tag ) ; } returns returns throws componentPathIsValid ( componentPath ) { return ! _ . isEmpty ( this . getMatchingComponents ( componentPath ) ) ; } returns throws getMatchingComponents ( componentPath ) { return < ComponentTag [ ] > _ . map ( this . getMatchingPaths ( componentPath ) , _ . last ) ; } returns throws getMatchingPaths ( componentPath ) { if ( this . noApp ) { return [ _ . map ( componentPath . nodes ( ) , ( fqtag ) => ( { fqtag , tag : fqtag } ) ) ] ; } const firstTag = componentPath . first ( ) ; if ( ! ( firstTag in this . componentTable ) ) { return [ ] ; } const matchingNode = { fqtag : firstTag , tag : firstTag , content : this . getContent ( { fqtag : firstTag , tag : firstTag } ) } ; if ( componentPath . length ( ) === 1 && firstTag in this . componentTable ) { return [ [ matchingNode ] ] ; } return this . _getMatchingPaths ( componentPath . tail ( ) , matchingNode . content ) . map ( ( matchingPath ) => [ matchingNode , ... matchingPath ] ) ; } throws private _getMatchingPaths ( componentPath , componentAst ) { if ( _ . isEmpty ( componentAst ) ) { return [ ] ; } const matchingNodes = componentAst . filter ( ( at ) => at . fqtag === componentPath . first ( ) ) . map ( ( matchingNode ) => _ . assign ( matchingNode , { content : this . getContent ( matchingNode ) } ) ) ; if ( componentPath . length ( ) === 1 ) { return _ . map ( matchingNodes , ( matchingNode ) : ComponentTagPath => [ matchingNode ] ) ; } return _ . flatMap ( matchingNodes , ( matchingNode ) : ComponentTagPath [ ] => _ . map ( this . _getMatchingPaths ( componentPath . tail ( ) , matchingNode . content ) , ( matchingPath ) => [ matchingNode , ... matchingPath ] ) ) ; } param componentTag returns throws private getContent ( componentTag ) { let ret ; if ( ComponentHelper . IsDvIncludeComponent ( componentTag ) ) { const includedComponentTag = ComponentHelper . GetIncludedComponentTag ( componentTag ) ; if ( includedComponentTag === null ) { ret = [ ] ; } else { ComponentHelper . ComponentExistsOrFail ( includedComponentTag , this . componentTable ) ; const childDvOf = ComponentHelper . GetDvOfForChild ( componentTag , includedComponentTag ) ; const childComponentTag = { ... includedComponentTag , context : { } , ... { dvOf : childDvOf } } ; ret = [ childComponentTag ] ; } } else if ( ComponentHelper . IsDvTxComponent ( componentTag ) ) { ret = componentTag . content ; } else { /**
             * By default, the context of the children of this component will be this
             * component's inputs. But if we are passing along a component and there's
             * a value for that in the context, we need to detect that and replace the
             * value with the component input. (If otherwise, we'd loose the
             * information)
             */ const childContext = componentTag . inputs ; _ . each ( componentTag . inputs , ( inputValue , inputName ) => { /**
                 * If one of the inputs has a variable as a value and we have that
                 * value defined in the context and the value happens to be a component
                 * then replace the value of that input with the component literal.
                 */ if ( _ . has ( componentTag . context , inputValue ) ) { try { const componentExpr = ComponentHelper . ParseComponentExpr ( componentTag . context ! [ inputValue ] ) ; childContext ! [ inputName ] = JSON . stringify ( ComponentHelper . GetComponentInput ( componentExpr , componentTag . context ) ) ; } catch ( e ) { } } } ) ; ComponentHelper . ComponentExistsOrFail ( componentTag , this . componentTable ) ; ret = _ . map ( this . componentTable [ componentTag . tag ] , ( at ) => { const childComponentTag = { ... at , context : childContext } ; childComponentTag . dvOf = ComponentHelper . GetDvOfForChild ( componentTag , childComponentTag ) ; return childComponentTag ; } ) ; } const contentTags = _ . map ( ret , 'tag' ) ; if ( _ . includes ( contentTags , 'router-outlet' ) ) { const routeComponents = _ . uniqBy ( this . getRouteComponents ( this . componentTable ) , 'fqtag' ) ; ret = < ComponentTag [ ] > _ . concat ( < ComponentTag [ ] > ret , routeComponents ) ; } return ret ; } toString ( ) { return JSON . stringify ( this . componentTable , null , INDENT_NUM_SPACES ) ; } return private getRouteComponents ( componentTable ) { return _ . map ( this . routeComponentSelectors , ( component ) => { if ( ! _ . has ( componentTable , component ) ) { throw new Error ( `Route component ${ component } doesn't exist` ) ; } return { fqtag : component , tag : component , content : componentTable [ component ] , context : { } } ; } ) ; } }