license import * as ts from 'typescript' ; import { Change , InsertChange , NoopChange } from './change' ; param fileToEdit param symbolName param fileName param isDefault return export function insertImport ( source , fileToEdit , symbolName , fileName , isDefault = false ) { const rootNode = source ; const allImports = findNodes ( rootNode , ts . SyntaxKind . ImportDeclaration ) ; const relevantImports = allImports . filter ( node => { const importFiles = node . getChildren ( ) . filter ( child => child . kind === ts . SyntaxKind . StringLiteral ) . map ( n => ( n as ts . StringLiteral ) . text ) ; return importFiles . filter ( file => file === fileName ) . length === 1 ; } ) ; if ( relevantImports . length > 0 ) { let importsAsterisk = false ; const imports = [ ] ; relevantImports . forEach ( n => { Array . prototype . push . apply ( imports , findNodes ( n , ts . SyntaxKind . Identifier ) ) ; if ( findNodes ( n , ts . SyntaxKind . AsteriskToken ) . length > 0 ) { importsAsterisk = true ; } } ) ; if ( importsAsterisk ) { return new NoopChange ( ) ; } const importTextNodes = imports . filter ( n => ( n as ts . Identifier ) . text === symbolName ) ; if ( importTextNodes . length === 0 ) { const fallbackPos = findNodes ( relevantImports [ 0 ] , ts . SyntaxKind . CloseBraceToken ) [ 0 ] . getStart ( ) || findNodes ( relevantImports [ 0 ] , ts . SyntaxKind . FromKeyword ) [ 0 ] . getStart ( ) ; return insertAfterLastOccurrence ( imports , `, ${ symbolName }` , fileToEdit , fallbackPos ) ; } return new NoopChange ( ) ; } const useStrict = findNodes ( rootNode , ts . SyntaxKind . StringLiteral ) . filter ( ( n ) => n . text === 'use strict' ) ; let fallbackPos = 0 ; if ( useStrict . length > 0 ) { fallbackPos = useStrict [ 0 ] . end ; } const open = isDefault ? '' : '{ ' ; const close = isDefault ? '' : ' }' ; const insertAtBeginning = allImports . length === 0 && useStrict . length === 0 ; const separator = insertAtBeginning ? '' : ';\n' ; const toInsert = `${ separator }import ${ open }${ symbolName }${ close }` + ` from '${ fileName }'${ insertAtBeginning ? ';\n' : '' }` ; return insertAfterLastOccurrence ( allImports , toInsert , fileToEdit , fallbackPos , ts . SyntaxKind . StringLiteral ) ; } param node param kind param max return export function findNodes ( node , kind , max = Infinity ) { if ( ! node || max == 0 ) { return [ ] ; } const arr = [ ] ; if ( node . kind === kind ) { arr . push ( node ) ; max -- ; } if ( max > 0 ) { for ( const child of node . getChildren ( ) ) { findNodes ( child , kind , max ) . forEach ( node => { if ( max > 0 ) { arr . push ( node ) ; } max -- ; } ) ; if ( max <= 0 ) { break ; } } } return arr ; } param sourceFile returns { Observable < ts . Node > } export function getSourceNodes ( sourceFile ) { const nodes = [ sourceFile ] ; const result = [ ] ; while ( nodes . length > 0 ) { const node = nodes . shift ( ) ; if ( node ) { result . push ( node ) ; if ( node . getChildCount ( sourceFile ) >= 0 ) { nodes . unshift ( ... node . getChildren ( ) ) ; } } } return result ; } export function findNode ( node , kind , text ) { if ( node . kind === kind && node . getText ( ) === text ) { return node ; } let foundNode = null ; ts . forEachChild ( node , childNode => { foundNode = foundNode || findNode ( childNode , kind , text ) ; } ) ; return foundNode ; } return function nodesByPosition ( first , second ) { return first . getStart ( ) - second . getStart ( ) ; } param nodes param toInsert param file param fallbackPos param syntaxKind return throw export function insertAfterLastOccurrence ( nodes , toInsert , file , fallbackPos , syntaxKind ? ) { let lastItem = [ ... nodes ] . sort ( nodesByPosition ) . pop ( ) ; if ( ! lastItem ) { throw new Error ( ) ; } if ( syntaxKind ) { lastItem = findNodes ( lastItem , syntaxKind ) . sort ( nodesByPosition ) . pop ( ) ; } if ( ! lastItem && fallbackPos == undefined ) { throw new Error ( `tried to insert ${ toInsert } as first occurence with no fallback position` ) ; } const lastItemPosition = lastItem ? lastItem . getEnd ( ) : fallbackPos ; return new InsertChange ( file , lastItemPosition , toInsert ) ; } export function getContentOfKeyLiteral ( _source , node ) { if ( node . kind == ts . SyntaxKind . Identifier ) { return ( node as ts . Identifier ) . text ; } else if ( node . kind == ts . SyntaxKind . StringLiteral ) { return ( node as ts . StringLiteral ) . text ; } else { return null ; } } function _angularImportsFromNode ( node , _sourceFile ) { const ms = node . moduleSpecifier ; let modulePath ; switch ( ms . kind ) { case ts . SyntaxKind . StringLiteral : modulePath = ( ms as ts . StringLiteral ) . text ; break ; default : return { } ; } if ( ! modulePath . startsWith ( '@angular/' ) ) { return { } ; } if ( node . importClause ) { if ( node . importClause . name ) { return { } ; } else if ( node . importClause . namedBindings ) { const nb = node . importClause . namedBindings ; if ( nb . kind == ts . SyntaxKind . NamespaceImport ) { return { [ ( nb as ts . NamespaceImport ) . name . text + '.' ] : modulePath , } ; } else { const namedImports = nb as ts . NamedImports ; return namedImports . elements . map ( ( is ) => is . propertyName ? is . propertyName . text : is . name . text ) . reduce ( ( acc , curr ) => { acc [ curr ] = modulePath ; return acc ; } , { } ) ; } } return { } ; } else { return { } ; } } export function getDecoratorMetadata ( source , identifier , module ) { const angularImports = findNodes ( source , ts . SyntaxKind . ImportDeclaration ) . map ( ( node ) => _angularImportsFromNode ( node , source ) ) . reduce ( ( acc , current ) => { for ( const key of Object . keys ( current ) ) { acc [ key ] = current [ key ] ; } return acc ; } , { } ) ; return getSourceNodes ( source ) . filter ( node => { return node . kind == ts . SyntaxKind . Decorator && ( node as ts . Decorator ) . expression . kind == ts . SyntaxKind . CallExpression ; } ) . map ( node => ( node as ts . Decorator ) . expression as ts . CallExpression ) . filter ( expr => { if ( expr . expression . kind == ts . SyntaxKind . Identifier ) { const id = expr . expression as ts . Identifier ; return id . getFullText ( source ) == identifier && angularImports [ id . getFullText ( source ) ] === module ; } else if ( expr . expression . kind == ts . SyntaxKind . PropertyAccessExpression ) { const paExpr = expr . expression as ts . PropertyAccessExpression ; if ( paExpr . expression . kind !== ts . SyntaxKind . Identifier ) { return false ; } const id = paExpr . name . text ; const moduleId = ( paExpr . expression as ts . Identifier ) . getText ( source ) ; return id === identifier && ( angularImports [ moduleId + '.' ] === module ) ; } return false ; } ) . filter ( expr => expr . arguments [ 0 ] && expr . arguments [ 0 ] . kind == ts . SyntaxKind . ObjectLiteralExpression ) . map ( expr => expr . arguments [ 0 ] as ts . ObjectLiteralExpression ) ; } function findClassDeclarationParent ( node ) { if ( ts . isClassDeclaration ( node ) ) { return node ; } return node . parent && findClassDeclarationParent ( node . parent ) ; } param source NgModule returns NgModule export function getFirstNgModuleName ( source ) { const ngModulesMetadata = getDecoratorMetadata ( source , 'NgModule' , '@angular/core' ) ; if ( ngModulesMetadata . length === 0 ) { return undefined ; } const moduleClass = findClassDeclarationParent ( ngModulesMetadata [ 0 ] ) ; if ( ! moduleClass || ! moduleClass . name ) { return undefined ; } return moduleClass . name . text ; } export function addSymbolToNgModuleMetadata ( source , ngModulePath , metadataField , symbolName , importPath = null ) { const nodes = getDecoratorMetadata ( source , 'NgModule' , '@angular/core' ) ; let node = nodes [ 0 ] ; if ( ! node ) { return [ ] ; } const matchingProperties = ( node as ts . ObjectLiteralExpression ) . properties . filter ( prop => prop . kind == ts . SyntaxKind . PropertyAssignment ) . filter ( ( prop ) => { const name = prop . name ; switch ( name . kind ) { case ts . SyntaxKind . Identifier : return ( name as ts . Identifier ) . getText ( source ) == metadataField ; case ts . SyntaxKind . StringLiteral : return ( name as ts . StringLiteral ) . text == metadataField ; } return false ; } ) ; if ( ! matchingProperties ) { return [ ] ; } if ( matchingProperties . length == 0 ) { const expr = node as ts . ObjectLiteralExpression ; let position ; let toInsert ; if ( expr . properties . length == 0 ) { position = expr . getEnd ( ) - 1 ; toInsert = `  ${ metadataField }: [${ symbolName }]\n` ; } else { node = expr . properties [ expr . properties . length - 1 ] ; position = node . getEnd ( ) ; const text = node . getFullText ( source ) ; const matches = text . match ( /^\r?\n\s*/ ) ; if ( matches && matches . length > 0 ) { toInsert = `,${ matches [ 0 ] }${ metadataField }: [${ symbolName }]` ; } else { toInsert = `, ${ metadataField }: [${ symbolName }]` ; } } if ( importPath !== null ) { return [ new InsertChange ( ngModulePath , position , toInsert ) , insertImport ( source , ngModulePath , symbolName . replace ( /\..*$/ , '' ) , importPath ) , ] ; } else { return [ new InsertChange ( ngModulePath , position , toInsert ) ] ; } } const assignment = matchingProperties [ 0 ] as ts . PropertyAssignment ; if ( assignment . initializer . kind !== ts . SyntaxKind . ArrayLiteralExpression ) { return [ ] ; } const arrLiteral = assignment . initializer as ts . ArrayLiteralExpression ; if ( arrLiteral . elements . length == 0 ) { node = arrLiteral ; } else { node = arrLiteral . elements ; } if ( ! node ) { console . error ( 'No app module found. Please add your new class to your component.' ) ; return [ ] ; } if ( Array . isArray ( node ) ) { const nodeArray = node as { } as Array < ts . Node > ; const symbolsArray = nodeArray . map ( node => node . getText ( ) ) ; if ( symbolsArray . includes ( symbolName ) ) { return [ ] ; } node = node [ node . length - 1 ] ; } let toInsert ; let position = node . getEnd ( ) ; if ( node . kind == ts . SyntaxKind . ObjectLiteralExpression ) { const expr = node as ts . ObjectLiteralExpression ; if ( expr . properties . length == 0 ) { position = expr . getEnd ( ) - 1 ; toInsert = `  ${ symbolName }\n` ; } else { const text = node . getFullText ( source ) ; if ( text . match ( /^\r?\r?\n/ ) ) { toInsert = `,${ text . match ( /^\r?\n\s*/ ) [ 0 ] }${ symbolName }` ; } else { toInsert = `, ${ symbolName }` ; } } } else if ( node . kind == ts . SyntaxKind . ArrayLiteralExpression ) { position -- ; toInsert = `${ symbolName }` ; } else { const text = node . getFullText ( source ) ; if ( text . match ( /^\r?\n/ ) ) { toInsert = `,${ text . match ( /^\r?\n(\r?)\s*/ ) [ 0 ] }${ symbolName }` ; } else { toInsert = `, ${ symbolName }` ; } } if ( importPath !== null ) { return [ new InsertChange ( ngModulePath , position , toInsert ) , insertImport ( source , ngModulePath , symbolName . replace ( /\..*$/ , '' ) , importPath ) , ] ; } return [ new InsertChange ( ngModulePath , position , toInsert ) ] ; } /**
 * Custom function to insert a declaration (component, pipe, directive)
 * into NgModule declarations. It also imports the component.
 */ export function addDeclarationToModule ( source , modulePath , classifiedName , importPath ) { return addSymbolToNgModuleMetadata ( source , modulePath , 'declarations' , classifiedName , importPath ) ; } /**
 * Custom function to insert an NgModule into NgModule imports. It also imports the module.
 */ export function addImportToModule ( source , modulePath , classifiedName , importPath ) { return addSymbolToNgModuleMetadata ( source , modulePath , 'imports' , classifiedName , importPath ) ; } /**
 * Custom function to insert a provider into NgModule. It also imports it.
 */ export function addProviderToModule ( source , modulePath , classifiedName , importPath ) { return addSymbolToNgModuleMetadata ( source , modulePath , 'providers' , classifiedName , importPath ) ; } /**
 * Custom function to insert an export into NgModule. It also imports it.
 */ export function addExportToModule ( source , modulePath , classifiedName , importPath ) { return addSymbolToNgModuleMetadata ( source , modulePath , 'exports' , classifiedName , importPath ) ; } /**
 * Custom function to insert an export into NgModule. It also imports it.
 */ export function addBootstrapToModule ( source , modulePath , classifiedName , importPath ) { return addSymbolToNgModuleMetadata ( source , modulePath , 'bootstrap' , classifiedName , importPath ) ; } /**
 * Custom function to insert an entryComponent into NgModule. It also imports it.
 */ export function addEntryComponentToModule ( source , modulePath , classifiedName , importPath ) { return addSymbolToNgModuleMetadata ( source , modulePath , 'entryComponents' , classifiedName , importPath ) ; } /**
 * Determine if an import already exists.
 */ export function isImported ( source , classifiedName , importPath ) { const allNodes = getSourceNodes ( source ) ; const matchingNodes = allNodes . filter ( node => node . kind === ts . SyntaxKind . ImportDeclaration ) . filter ( ( imp ) => imp . moduleSpecifier . kind === ts . SyntaxKind . StringLiteral ) . filter ( ( imp ) => { return ( imp . moduleSpecifier as ts . StringLiteral ) . text === importPath ; } ) . filter ( ( imp ) => { if ( ! imp . importClause ) { return false ; } const nodes = findNodes ( imp . importClause , ts . SyntaxKind . ImportSpecifier ) . filter ( n => n . getText ( ) === classifiedName ) ; return nodes . length > 0 ; } ) ; return matchingNodes . length > 0 ; }