...
1<#
2.SYNOPSIS
3Update unified ToC file for publishing reference docs on docs.microsoft.com
4
5.DESCRIPTION
6Given a doc repo location and a location to output the ToC generate a Unified
7Table of Contents:
8
9* Get list of packages onboarded to docs.microsoft.com (domain specific)
10* Get metadata for onboarded packages from metadata CSV
11* Build a sorted list of services
12* Add ToC nodes for the service
13* Add "Core" packages to the bottom of the ToC under "Other"
14
15ToC node layout:
16* Service (service level overview page)
17 * Client Package 1 (package level overview page)
18 * Client Package 2 (package level overview page)
19 ...
20 * Management
21 * Management Package 1
22 * Management Package 2
23 ...
24
25.PARAMETER DocRepoLocation
26Location of the documentation repo. This repo may be sparsely checked out
27depending on the requirements for the domain
28
29.PARAMETER OutputLocation
30Output location for unified reference yml file
31
32#>
33
34param(
35 [Parameter(Mandatory = $true)]
36 [string] $DocRepoLocation,
37
38 [Parameter(Mandatory = $true)]
39 [string] $OutputLocation
40)
41. $PSScriptRoot/common.ps1
42. $PSScriptRoot/Helpers/PSModule-Helpers.ps1
43
44Install-ModuleIfNotInstalled "powershell-yaml" "0.4.1" | Import-Module
45
46Set-StrictMode -Version 3
47
48function GetClientPackageNode($clientPackage) {
49 $packageInfo = &$GetDocsMsTocDataFn `
50 -packageMetadata $clientPackage `
51 -docRepoLocation $DocRepoLocation
52
53 return [PSCustomObject]@{
54 name = $packageInfo.PackageTocHeader
55 href = $packageInfo.PackageLevelReadmeHref
56 # This is always one package and it must be an array
57 children = $packageInfo.TocChildren
58 };
59}
60
61function GetPackageKey($pkg) {
62 $pkgKey = $pkg.Package
63 $groupId = $null
64
65 if ($pkg.PSObject.Members.Name -contains "GroupId") {
66 $groupId = $pkg.GroupId
67 }
68
69 if ($groupId) {
70 $pkgKey = "${groupId}:${pkgKey}"
71 }
72
73 return $pkgKey
74}
75
76function GetPackageLookup($packageList) {
77 $packageLookup = @{}
78
79 foreach ($pkg in $packageList) {
80 $pkgKey = GetPackageKey $pkg
81
82 # We want to prefer updating non-hidden packages but if there is only
83 # a hidden entry then we will return that
84 if (!$packageLookup.ContainsKey($pkgKey) -or $packageLookup[$pkgKey].Hide -eq "true") {
85 $packageLookup[$pkgKey] = $pkg
86 }
87 else {
88 # Warn if there are more then one non-hidden package
89 if ($pkg.Hide -ne "true") {
90 Write-Host "Found more than one package entry for $($pkg.Package) selecting the first non-hidden one."
91 }
92 }
93 }
94
95 return $packageLookup
96}
97
98$onboardedPackages = &$GetOnboardedDocsMsPackagesFn `
99 -DocRepoLocation $DocRepoLocation
100
101# This criteria is different from criteria used in `Update-DocsMsPackages.ps1`
102# because we need to generate ToCs for packages which are not necessarily "New"
103# in the metadata AND onboard legacy packages (which `Update-DocsMsPackages.ps1`
104# does not do)
105$fullMetadata = Get-CSVMetadata
106$metadata = @()
107foreach($metadataEntry in $fullMetadata) {
108 if ($metadataEntry.Package -and $metadataEntry.Hide -ne 'true') {
109 $pkgKey = GetPackageKey $metadataEntry
110 if($onboardedPackages.ContainsKey($pkgKey)) {
111 $metadata += $metadataEntry
112 }
113 }
114}
115
116$fileMetadata = @()
117foreach ($metadataFile in Get-ChildItem "$DocRepoLocation/metadata/*/*.json" -Recurse) {
118 $fileContent = Get-Content $metadataFile -Raw
119 $metadataEntry = ConvertFrom-Json $fileContent
120
121 if ($metadataEntry) {
122 $fileMetadata += $metadataEntry
123 }
124}
125
126# Add file metadata information to package metadata from metadata CSV. Because
127# metadata can exist for packages in both preview and GA there may be more than
128# one file metadata entry. If that is the case keep the first entry found. We
129# only use the `DirectoryPath` property from the json file metadata at this time
130for ($i = 0; $i -lt $metadata.Count; $i++) {
131 foreach ($fileEntry in $fileMetadata) {
132 if ($fileEntry.Name -eq $metadata[$i].Package) {
133 if ($metadata[$i].PSObject.Members.Name -contains "FileMetadata") {
134 Write-Host "File metadata already added for $($metadata[$i].Package). Keeping the first entry found."
135 continue
136 }
137 if (!($metadata[$i].PSObject.Members.Name -contains "GroupId") -or ($fileEntry.Group -eq $metadata[$i].GroupId)) {
138 Add-Member `
139 -InputObject $metadata[$i] `
140 -MemberType NoteProperty `
141 -Name FileMetadata `
142 -Value $fileEntry
143 }
144 }
145 }
146}
147
148$packagesForToc = @{}
149$allPackages = GetPackageLookup $metadata
150foreach ($metadataKey in $allPackages.Keys) {
151 $metadataEntry = $allPackages[$metadataKey]
152 if (!$metadataEntry.ServiceName) {
153 LogWarning "Empty ServiceName for package `"$metadataKey`". Skipping."
154 continue
155 }
156 $packagesForToc[$metadataKey] = $metadataEntry
157}
158
159# Get unique service names and sort alphabetically to act as the service nodes
160# in the ToC
161$services = @{}
162foreach ($package in $packagesForToc.Values) {
163 if ($package.ServiceName -eq 'Other') {
164 # Skip packages under the service category "Other". Those will be handled
165 # later
166 continue
167 }
168 if (!$services.ContainsKey($package.ServiceName)) {
169 $services[$package.ServiceName] = $true
170 }
171}
172$serviceNameList = $services.Keys | Sort-Object
173
174$toc = @()
175foreach ($service in $serviceNameList) {
176 Write-Host "Building service: $service"
177
178 $packageItems = @()
179
180 # Client packages get individual entries
181 $clientPackages = $packagesForToc.Values.Where({ $_.ServiceName -eq $service -and ('client' -eq $_.Type) })
182 $clientPackages = $clientPackages | Sort-Object -Property Package
183 foreach ($clientPackage in $clientPackages) {
184 $packageItems += GetClientPackageNode -clientPackage $clientPackage
185 }
186
187 # All management packages go under a single `Management` header in the ToC
188 $mgmtPackages = $packagesForToc.Values.Where({ $_.ServiceName -eq $service -and ('mgmt' -eq $_.Type) })
189 $mgmtPackages = $mgmtPackages | Sort-Object -Property Package
190 if ($mgmtPackages) {
191 $children = &$GetDocsMsTocChildrenForManagementPackagesFn `
192 -packageMetadata $mgmtPackages `
193 -docRepoLocation $DocRepoLocation
194
195 $packageItems += [PSCustomObject]@{
196 name = 'Management'
197 # There could be multiple packages, ensure this is treated as an array
198 # even if it is a single package
199 children = @($children)
200 }
201 }
202
203 $uncategorizedPackages = $packagesForToc.Values.Where({ $_.ServiceName -eq $service -and !(@('client', 'mgmt') -contains $_.Type) })
204 if ($uncategorizedPackages) {
205 foreach ($package in $uncategorizedPackages) {
206 LogWarning "Uncategorized package for service: $service - $($package.Package). Package not onboarded."
207 }
208 }
209
210 $serviceReadmeBaseName = $service.ToLower().Replace(' ', '-').Replace('/', '-')
211 $serviceTocEntry = [PSCustomObject]@{
212 name = $service;
213 href = "~/docs-ref-services/{moniker}/$serviceReadmeBaseName.md"
214 landingPageType = 'Service'
215 items = @($packageItems)
216 }
217 $toc += $serviceTocEntry
218}
219
220# Core packages belong under the "Other" node in the ToC
221$otherPackageItems = New-Object -TypeName System.Collections.Generic.List[PSCustomObject]
222$otherPackages = $packagesForToc.Values.Where({ $_.ServiceName -eq 'Other' })
223$otherPackages = $otherPackages | Sort-Object -Property DisplayName
224
225if ($otherPackages) {
226 foreach ($otherPackage in $otherPackages) {
227 $segments = $otherPackage.DisplayName.Split('-').ForEach({ $_.Trim() })
228
229
230 if ($segments.Count -gt 1) {
231 $currentNode = $otherPackageItems
232
233 # Iterate up to the penultimate item in the array so that the final item
234 # in the array can be added as a leaf node. Since the array always has at
235 # least two elements this iteration will cover at least the first element.
236 # e.g. @(0, 1)[0..0] => 0
237 foreach ($segment in $segments[0..($segments.Count - 2)]) {
238 $matchingNode = $currentNode.Where({ $_.name -eq $segment })
239
240 # ToC nodes can be "branches" which contain 0 or more branch
241 # or leaf nodes in an "items" field OR they can be leaf nodes which have
242 # a "children" field which can only contain package names or namespaces.
243 # A node cannot contain both "items" and "children". If a node already
244 # has a "children" field then it is a leaf node and cannot take
245 # additional branch nodes.
246 # Children are added using the `GetClientPackageNode` function
247 if ($matchingNode -and $matchingNode.PSObject.Members.Name -contains "children") {
248 LogWarning "Cannot create nested entry for package $($otherPackage.Package) because Segment `"$segment`" in the DisplayName $($otherPackage.DisplayName) is already a leaf node. Excluding package: $($otherPackage.Package)"
249 $currentNode = $null
250 break
251 }
252
253 if ($matchingNode) {
254 $currentNode = $matchingNode[0].items
255 }
256 else {
257 $newNode = [PSCustomObject]@{
258 name = $segment
259 landingPageType = 'Service'
260 items = New-Object -TypeName System.Collections.Generic.List[PSCustomObject]
261 }
262 $currentNode.Add($newNode)
263 $currentNode = $newNode.items
264 }
265 }
266
267 if ($null -ne $currentNode) {
268 $otherPackage.DisplayName = $segments[$segments.Count - 1]
269 $currentNode.Add((GetClientPackageNode $otherPackage))
270 }
271
272 }
273 else {
274 $otherPackageItems.Add((GetClientPackageNode $otherPackage))
275 }
276 }
277}
278$toc += [PSCustomObject]@{
279 name = 'Other';
280 landingPageType = 'Service';
281 items = $otherPackageItems + @(
282 [PSCustomObject]@{
283 name = "Uncategorized Packages";
284 landingPageType = 'Service';
285 # All onboarded packages which have not been placed in the ToC will be
286 # handled by the docs system here. In this case the list would consist of
287 # packages whose ServiceName field is empty in the metadata.
288 children = @('**');
289 }
290 )
291}
292
293$output = @([PSCustomObject]@{
294 name = 'Reference';
295 landingPageType = 'Root';
296 expanded = $false;
297 items = $toc
298 })
299
300if (Test-Path "Function:$UpdateDocsMsTocFn") {
301 $output = &$UpdateDocsMsTocFn -toc $output
302}
303
304$outputYaml = ConvertTo-Yaml $output
305Set-Content -Path $OutputLocation -Value $outputYaml
View as plain text