Private/Angular/Setup/Edit-NgModule.ps1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
<############################################################################
 # 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
    }

}