Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 216 additions & 0 deletions Docs/mirror-local-docs.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
<#
.SYNOPSIS
Populates Docs/pages/docs from local sibling repositories.

.DESCRIPTION
Mirrors what Pipeline/Build.Pages.cs does, but reads each docs slice from a
sibling clone instead of the GitHub Contents API. Use this to preview the
site locally without a GitHub token (and without rate limits).

The script assumes every source repo is checked out as a sibling of
Testably.Site (same parent folder) and is on the latest main branch.
No git operations are performed.

.PARAMETER Root
Parent directory that contains all sibling repos. Defaults to the parent
of this repository.

.EXAMPLE
pwsh ./Docs/mirror-local-docs.ps1
pwsh ./Docs/mirror-local-docs.ps1 -Root C:\Work\src\GitHub
#>
[CmdletBinding()]
param(
[string]$Root
)

$ErrorActionPreference = "Stop"
Set-StrictMode -Version 2.0

$RepoRoot = Split-Path -Parent $PSScriptRoot
if ([string]::IsNullOrEmpty($Root)) {
$Root = Split-Path -Parent $RepoRoot
}

$DocsRoot = Join-Path $RepoRoot "Docs/pages/docs"

# Mirrors AggregatedSources in Pipeline/Build.Pages.cs.
$Sources = @(
[pscustomobject]@{ Repo="Testably.Abstractions"; SourcePath="Docs/pages/docs"; Target="Abstractions"; InlineReadme=$false; ExtraReadmes=@() }
[pscustomobject]@{ Repo="aweXpect"; SourcePath="Docs/pages"; Target="aweXpect"; InlineReadme=$false; ExtraReadmes=@(
[pscustomobject]@{ Repo="aweXpect.Migration"; TargetFile="09-migration.md" }
) }
[pscustomobject]@{ Repo="aweXpect.Json"; SourcePath="Docs/pages"; Target="Extensions/aweXpect.Json"; InlineReadme=$true; ExtraReadmes=@() }
[pscustomobject]@{ Repo="aweXpect.Mockolate"; SourcePath="Docs/pages"; Target="Extensions/aweXpect.Mockolate"; InlineReadme=$true; ExtraReadmes=@() }
[pscustomobject]@{ Repo="aweXpect.Reflection"; SourcePath="Docs/pages"; Target="Extensions/aweXpect.Reflection"; InlineReadme=$true; ExtraReadmes=@() }
[pscustomobject]@{ Repo="aweXpect.Testably"; SourcePath="Docs/pages"; Target="Extensions/aweXpect.Testably"; InlineReadme=$true; ExtraReadmes=@() }
[pscustomobject]@{ Repo="aweXpect.Web"; SourcePath="Docs/pages"; Target="Extensions/aweXpect.Web"; InlineReadme=$true; ExtraReadmes=@() }
[pscustomobject]@{ Repo="aweXpect.Chronology"; SourcePath="Docs/pages"; Target="Chronology"; InlineReadme=$true; ExtraReadmes=@() }
[pscustomobject]@{ Repo="Mockolate"; SourcePath="Docs/pages"; Target="Mockolate"; InlineReadme=$false; ExtraReadmes=@(
[pscustomobject]@{ Repo="Mockolate.Migration"; TargetFile="11-migration.md" }
) }
)

# Local clones often contain build output that the GitHub-API path would not
# see, so filter it out.
$ExcludeDirs = @("node_modules", ".docusaurus", "build", ".git")
$IndexRegex = [regex]'^(\d+)-index(\.mdx?)$'
$TextExt = @(".md", ".mdx", ".json", ".yml", ".yaml", ".txt", ".css", ".js", ".tsx", ".ts", ".svg")
$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)

function Strip-ReadmeFront([string]$readme) {
$lines = ($readme -replace "`r`n", "`n") -split "`n"
$i = 0
while ($i -lt $lines.Length -and [string]::IsNullOrWhiteSpace($lines[$i])) { $i++ }
if ($i -lt $lines.Length -and $lines[$i].TrimStart().StartsWith("# ")) { $i++ }
while ($i -lt $lines.Length -and [string]::IsNullOrWhiteSpace($lines[$i])) { $i++ }
while ($i -lt $lines.Length -and $lines[$i].TrimStart().StartsWith("[!")) { $i++ }
while ($i -lt $lines.Length -and [string]::IsNullOrWhiteSpace($lines[$i])) { $i++ }
if ($i -ge $lines.Length) { return "" }
return ($lines[$i..($lines.Length - 1)] -join "`n")
}

function Get-ReadmeIntro([string]$readmePath) {
if (-not (Test-Path -LiteralPath $readmePath)) { return "" }
$content = Get-Content -Raw -LiteralPath $readmePath
$idx = $content.IndexOf("`n##")
if ($idx -gt 0) { return $content.Substring($idx) }
return ""
}

function Get-ReadmeBody([string]$readmePath) {
if (-not (Test-Path -LiteralPath $readmePath)) { return "" }
$content = Get-Content -Raw -LiteralPath $readmePath
return Strip-ReadmeFront $content
}

function Ensure-SidebarPosition([string]$content, [int]$position) {
$startsWithFm = $content.StartsWith("---`n") -or $content.StartsWith("---`r`n")
if ($startsWithFm) {
$fmStart = $content.IndexOf("`n") + 1
$fmEnd = $content.IndexOf("`n---", $fmStart)
if ($fmEnd -gt 0) {
$fm = $content.Substring($fmStart, $fmEnd - $fmStart)
if ($fm -match "(?m)^sidebar_position\s*:") {
return $content
}
return $content.Insert($fmEnd, "`nsidebar_position: $position")
}
}
return "---`nsidebar_position: $position`n---`n`n$content"
}

if (Test-Path -LiteralPath $DocsRoot) {
Write-Host "Cleaning $DocsRoot"
Remove-Item -LiteralPath $DocsRoot -Recurse -Force
}
New-Item -ItemType Directory -Path $DocsRoot -Force | Out-Null

foreach ($source in $Sources) {
$sourceDir = Join-Path $Root (Join-Path $source.Repo $source.SourcePath)
$targetDir = Join-Path $DocsRoot $source.Target

Write-Host ""
Write-Host "== $($source.Repo) -> docs/$($source.Target)"

if (-not (Test-Path -LiteralPath $sourceDir)) {
Write-Warning " Source not found: $sourceDir - skipping"
continue
}

New-Item -ItemType Directory -Path $targetDir -Force | Out-Null

$readmeIntro = ""
if ($source.InlineReadme) {
$readmePath = Join-Path $Root (Join-Path $source.Repo "README.md")
$intro = Get-ReadmeIntro $readmePath
if (-not [string]::IsNullOrEmpty($intro)) {
$readmeIntro = $intro.Replace("Docs/pages/", "./")
}
}
$readmeIntroDone = [string]::IsNullOrEmpty($readmeIntro)

$extraReadmes = New-Object System.Collections.ArrayList
foreach ($extra in $source.ExtraReadmes) {
$extraPath = Join-Path $Root (Join-Path $extra.Repo "README.md")
$body = Get-ReadmeBody $extraPath
if ([string]::IsNullOrEmpty($body)) {
Write-Warning " ExtraReadme not found or empty: $extraPath"
}
[void]$extraReadmes.Add([pscustomobject]@{
TargetFile = $extra.TargetFile
Body = $body
Done = [string]::IsNullOrEmpty($body)
})
}

$sourceDirFull = (Resolve-Path -LiteralPath $sourceDir).ProviderPath.TrimEnd('\')
$files = Get-ChildItem -LiteralPath $sourceDirFull -Recurse -File | Where-Object {
$rel = $_.FullName.Substring($sourceDirFull.Length).TrimStart('\','/')
$segments = $rel -split '[\\/]'
$excluded = $false
foreach ($seg in $segments) {
if ($ExcludeDirs -contains $seg) { $excluded = $true; break }
}
-not $excluded
}

foreach ($file in $files) {
$name = $file.Name
$rel = $file.FullName.Substring($sourceDirFull.Length).TrimStart('\','/')
$relDir = Split-Path -Path $rel -Parent
$destDir = if ([string]::IsNullOrEmpty($relDir)) { $targetDir } else { Join-Path $targetDir $relDir }

$idxMatch = $IndexRegex.Match($name)
$sidebarPosition = $null
$targetName = $name
if ($idxMatch.Success) {
$targetName = "index" + $idxMatch.Groups[2].Value
$sidebarPosition = [int]$idxMatch.Groups[1].Value
}

New-Item -ItemType Directory -Path $destDir -Force | Out-Null
$destPath = Join-Path $destDir $targetName

$ext = [System.IO.Path]::GetExtension($name).ToLower()
$isText = $TextExt -contains $ext

if ($isText) {
$content = Get-Content -Raw -LiteralPath $file.FullName
if ($null -eq $content) { $content = "" }

if ($source.InlineReadme -and -not $readmeIntroDone -and $name.StartsWith("00-") -and $content.Contains("{README}")) {
$content = $content.Replace("{README}", $readmeIntro)
$readmeIntroDone = $true
Write-Host " Inlined README into $rel"
}

for ($i = 0; $i -lt $extraReadmes.Count; $i++) {
$cur = $extraReadmes[$i]
if ($cur.Done) { continue }
if ($name -eq $cur.TargetFile -and $content.Contains("{README}")) {
$content = $content.Replace("{README}", $cur.Body)
$cur.Done = $true
Write-Host " Inlined extra README from $($source.ExtraReadmes[$i].Repo) into $rel"
}
}

if ($null -ne $sidebarPosition) {
$content = Ensure-SidebarPosition $content $sidebarPosition
}

[System.IO.File]::WriteAllText($destPath, $content, $Utf8NoBom)
} else {
Copy-Item -LiteralPath $file.FullName -Destination $destPath -Force
}
}

foreach ($cur in $extraReadmes) {
if (-not $cur.Done) {
Write-Warning " ExtraReadme '$($cur.TargetFile)' was never substituted (no matching file with {README} placeholder)"
}
}
}

Write-Host ""
Write-Host "Done. Output at: $DocsRoot"
10 changes: 10 additions & 0 deletions Docs/pages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ The documentation site for [Testably.Abstractions](https://github.com/Testably/T

## Local development

The `docs/` folder is gitignored and must be populated before starting the dev server. Use either `./build.ps1 Pages` from the repo root (fetches from GitHub via the Contents API; set the `GithubToken` parameter to avoid rate limits) or, for token-free local mirroring, [`Docs/mirror-local-docs.ps1`](../mirror-local-docs.ps1):

```powershell
pwsh ./Docs/mirror-local-docs.ps1
```

The script copies each docs slice from a sibling clone instead of fetching from GitHub. It assumes every source repo from `Pipeline/Build.Pages.cs` is checked out as a sibling of `Testably.Site` and on the latest `main` (no `git` operations are performed). Pass `-Root <path>` to override the parent directory if your clones live elsewhere. Stop the dev server before re-running — it holds file handles on `docs/`.

Then start the site:

```powershell
npm install
npm run start
Expand Down
Loading