Private/Angular/Setup/Edit-NgModule.ps1

<############################################################################
 # Update angular app.module.ts to include a new angular component
 # with a new "import" statement and add class to "declarations" section
 ############################################################################>

Function Edit-NgModuleAddComponent([WebCsprojInfo]$webCsprojInfo, [string]$className, [string]$importFromFile)
{
    Edit-NgModule $webCsprojInfo.appModuleFile "$($className)" $importFromFile "declarations: \[" "$($className)"
}

<############################################################################
 # Update angular app.module.ts to include a new angular service
 # with a new "import" statement and add class to "providers" section
 ############################################################################>

Function Edit-NgModuleAddService([WebCsprojInfo]$webCsprojInfo, [string]$className, [string]$importFromFile)
{
    # Make sure a "providers" section exists
    Edit-NgModuleAddSection $webCsprojInfo.appModuleFile "providers"    
    Edit-NgModule $webCsprojInfo.appModuleFile "$($className)" $importFromFile "providers: \[" "$($className)"
}

<############################################################################
 # Update angular app.module.ts to include importing a new library
 # with a new "import" statement and add class to "imports" section
 ############################################################################>

Function Edit-NgModuleAddImport([WebCsprojInfo]$webCsprojInfo, [string]$className, [string]$importFromFile, [string]$classToInsert)
{
    if([string]::IsNullOrWhitespace($classToInsert)) {
        $classToInsert = $className
    }
    Edit-NgModule $webCsprojInfo.appModuleFile "$($className)" $importFromFile "imports: \[" "$classToInsert"
}

<############################################################################
 # Update angular app-routing.module.ts with new route and
 # importing the component with an "import" statement
 ############################################################################>

Function Edit-NgModuleAddRoute([WebCsprojInfo]$webCsprojInfo, [string]$className, [string]$importFromFile, [string]$desiredUrl)
{
    if($webCsprojInfo.angularStyle -eq "ANGULAR_IO") {
        Edit-NgModule $webCsprojInfo.appRoutingFile "$($className)" $importFromFile "const routes: Routes = \[" " { path: '$($desiredUrl)', component: $($className) }"
    } else {
        Edit-NgModule $webCsprojInfo.appRoutingFile "$($className)" $importFromFile "RouterModule.forRoot\(\[" " { path: '$($desiredUrl)', component: $($className) }"
        # Make sure default route is last in route list
        Edit-NgRouteListDefault $webCsprojInfo.appRoutingFile
    }
}


<############################################################################
 # Update angular app.module.ts or app-routing.module.ts by adding a
 # new "import { ..." line up top and adding a class or string to one of the other
 # JSON arrays in the body of the file. Update the file in place.
 #
 # Arguments:
 # - $moduleFileName - name of app.module.ts or app-routing.module.ts file fully qualified
 # - $classNamem - name of TypeScript class we want to add, e.g. a component or service
 # - $importFromRelFile - relative path to location of source for this class in Angular terms
 # - $headerPattern - look for this line like "imports: [ ", and add an entry to this section
 # - $insertMe - string to add, either a Component, or maybe a routing line
 #
 # Algorithm:
 # - first update file so all TypeScript array markers "[" have a newline after
 # and "]" have a newline before. This makes our simple parser work better
 # - keep track of each line that looks like "import { ....", and record the index
 # of that last line that matches that pattern. We'll insert a new import directly
 # after this
 # - then look for the line that matches the header, e.g. "import: [ "
 # - once we're inside the header, look for the closing "]", but keep the index
 # of the last nonblank line within that section, possibly none if the array is empty.
 # - some of the middle lines of the section will have children arrays, e.g.
 # @NgModule({ // ignore this line
 # imports: [ // header matches, we're in the right section
 # BrowserModule, // in section, haven't found closing "]" yet
 # FormsModule, // in section, haven't found closing "]" yet
 # AgGridModule.withComponents( [ // in section, start of child array
 # 'RedComponent' // in section, middle of child array
 # ]), // in section, end of child array *NOT* end of section
 # FormsModule // out of child array, back in main section, this is the last nonblank line
 # // in main section, blank row
 # ], // end of main section
 # In this case we'll add a comma after "FormsModule" and insert a new entry after this.
 #
 # NOTES:
 # - this is *NOT* a real parser, that was way too hard.
 # - it's possible that other valid TypeScript will mess up this trivial algorithm
 ############################################################################>

Function Edit-NgModule([string]$moduleFileName, [string]$className, [string]$importFromRelFile, [string]$headerPattern, [string]$insertMe)
{
    # Force a newline after "[" and before "]" to make parsing easier
    (Get-Content $moduleFileName) `
        -replace '(\[)(.*)([^ \t])(.*)$', "`$1`r`n`t`$2`$3`$4" `
        -replace '^(.*)([^ \t])(.*)(\])', "`$1`$2`$3`r`n`t`$4" |
        Out-FileUtf8NoBom $moduleFileName
    
    $lines = (Get-Content $moduleFileName)

    # Go through all lines, find the last one that matches "import {..."
    $lastImportAt = -1;
    $thisLine = 0;
    $alreadyThere = $false
    foreach($line in $lines) {
        if($line -match "import\s+{\s*$className\s*}") {
            # Hey it's already there, don't add it
            $alreadyThere = $true
        } elseif($line -match "import {") {
            $lastImportAt = $thisLine;
        }
        $thisLine++;
    }
    if(-not $alreadyThere) {
        # Insert new import after last
        $lastImportLine = $lines[$lastImportAt]
        $lines[$lastImportAt] = $lastImportLine + "`r`nimport { $className } from '$importFromRelFile';"
    }

    # Look for header e.g."imports: [",
    # and find last nonblank entry before "]",
    # but ignore children records like "AgGridModule.withComponents([`r`n])"
    $thisLine = 0;
    $headerIndex = -1
    $footerIndex = -1
    $lastNonBlankIndex = -1
    $inChild = $false
    $alreadyThere = $false

    foreach($line in $lines) {
        if($headerIndex -eq -1) {
            # haven't started yet
            if($line -match $headerPattern) {
                # Matched header, let's track it
                $headerIndex = $thisLine;
            } 
        } else {
            # we're in the section
            
            # check if lines are the same, ignoring commas and after trimming
            if($line.Trim().ToUpper().Replace(",", "") -eq $insertMe.Trim().ToUpper().Replace(",", "") ) {
                $alreadyThere = $true
                break
            }

            if( ($inChild -eq $false) -and ($line -match "\[") ) {
                $inChild = $true
            } elseif( ($inChild -eq $true) -and ($line -match "\]") ) {
                $inChild = $false
                $lastNonBlankIndex = $thisLine
            } elseif( ($inChild -eq $false) -and ($line -match "\]") ) {
                $footerIndex = $thisLine
                break
            } else {
                if(-not [string]::IsNullOrWhitespace($line)) {
                    $lastNonBlankIndex = $thisLine
                }
            }
        }

        $thisLine++
    }
    
    if(-not $alreadyThere) {
        if($headerIndex -eq -1) {
            throw "Can't find start sequence '$headerPattern' when attempting to update '$moduleFileName'"
        }
        if($footerIndex -eq -1) {
            throw "Can't find end sequence ']' for start sequence '$headerPattern' when attempting to update '$moduleFileName'"
        }
        if($inChild -eq $true) {
            throw "Got confused in child entry for start sequence '$headerPattern' when attempting to update '$moduleFileName'"
        }

        # Special case - was array empty?
        if($lastNonBlankIndex -eq -1)
        {
            # Array was empty, but brackets were there
            $lines[$headerIndex] = "`t`t" + $lines[$headerIndex] + "`r`n`t$($insertMe)"
        }
        else
        {
            $priorLastLine = $lines[$lastNonBlankIndex].Trim()
            # Add comma to end of last line, soon to be second-to-last-line, if needed
            if(-not ($priorLastLine -match ",\s*$")) {
                $priorLastLine = "`t`t$($priorLastLine), "
            }

            # Add new line to end of this line
            $priorLastLine = $priorLastLine + "`r`n`t`t$($insertMe)"
            $lines[$lastNonBlankIndex] = $priorLastLine
        }
    }    

    $lines | Out-FileUtf8NoBom $moduleFileName
}



<############################################################################
 # Update angular app.module.ts or related to make sure it has a "providers"
 # section or whatever section we pass in
 ############################################################################>

Function Edit-NgModuleAddSection([string] $appModuleFile, [string]$sectionName) {

    [string]$contents = Get-Content -raw $appModuleFile
    if(-not ($contents -match "$($sectionName):")) {
        # "providers:" section is missing, add it
        $lines = Get-Content $appModuleFile
        $index = 0
        $exportClassLine = -1
        for($index = 0; $index -lt $lines.length; $index++) {
            if($lines[$index] -match "export class") {
                $exportClassLine = $index
                break
            }
        }

        if($exportClassLine -ge 0) {
            # We found the bottom "export class" line
            # if previous line is "})" and prior to that "]",
            # that's where we'll add this
            if($lines[$exportClassLine - 1] -match "^\s*}\s*\)\s*$") {
                if($lines[$exportClassLine - 2] -match "^\s*\]\s*,\s*$") {
                    # Found final "]" and it already had a comma
                    $lines[$exportClassLine - 2] = $lines[$exportClassLine - 2] + "`r`n $($sectionName): [`r`n ]"
                } elseif($lines[$exportClassLine - 2] -match "^\s*\]\s*$") {
                    # Found final "]" and it doesn't have a comma
                    $lines[$exportClassLine - 2] = $lines[$exportClassLine - 2] + ", `r`n $($sectionName): [`r`n ]" 
                }
                $lines | Out-FileUtf8NoBom $appModuleFile
            }
        }
    }
}



<############################################################################
 # Move ** route to bottom of route list. This can happen with generated code.
 # Update file in place.
 #
 # Example:
 #
 # RouterModule.forRoot([
 # { path: '', redirectTo: 'home', pathMatch: 'full' },
 # { path: 'home', component: HomeComponent },
 # { path: '**', redirectTo: 'home' }, # <------ this is bad, all further routes ignored, move to end
 # { path: 'counter', component: CounterComponent },=
 # ])
 ############################################################################>

Function Edit-NgRouteListDefault([string] $appModuleFile) {
    [string[]]$lines = Get-Content $appModuleFile
    
    [int]$index = -1 
    [int]$starStarIndex = -1
    [int]$lastPathRouteIndex = -1
    for($index = 0; $index -lt $lines.Length; $index++) {
        $line = $lines[$index]
        if($line -match "path: '\*\*'") {
            $starStarIndex = $index
        }
        if($line -match "path: '") {
            $lastPathRouteIndex = $index
        }
    }

    if( ($starStarIndex -gt 0) -and ($lastPathRouteIndex -gt 0) -and ($lastPathRouteIndex -gt $starStarIndex) ) {
        # We have a problem, routes below ** are skipped
        if($lines[$lastPathRouteIndex] -match ",\s*$") {
            # last route already has a final comma
        } else {
            $lines[$lastPathRouteIndex] = $lines[$lastPathRouteIndex] + ","
        }

        $starStarLine = $lines[$starStarIndex]
        $lines[$lastPathRouteIndex] = $lines[$lastPathRouteIndex] + "`r`n" + $starStarLine
        
        # Remove old ** line, convert to ArrayList first
        $linesAsArrayList = [System.Collections.ArrayList]$lines
        $linesAsArrayList.RemoveRange($starStarIndex, 1)
        $lines = [string[]]$linesAsArrayList

        $lines | Out-FileUtf8NoBom $appModuleFile
    }

}