Add GitHub Action to automate Monaco Editor updates

Add a workflow_dispatch-triggered GitHub Action that automates the
Monaco Editor update process documented in FilePreviewCommon.md:
- Downloads latest (or specified) Monaco version from npm
- Replaces monacoSRC/min/ with the new version
- Uses Puppeteer to run generateLanguagesJson.html and regenerate
  monaco_languages.json (identical code path to manual process)
- Runs ~60 validation assertions across 7 test groups
- Creates a PR with the changes via peter-evans/create-pull-request

Files added:
- .github/workflows/update-monaco-editor.yml
- .github/scripts/update-monaco-editor.ps1
- .github/scripts/generate-monaco-languages.js
- .github/scripts/tests/validate-monaco-update.tests.ps1

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Clint Rutkas
2026-03-31 21:11:55 -07:00
parent 7cc4a16aa7
commit ef53ea360f
4 changed files with 898 additions and 0 deletions

View File

@@ -0,0 +1,371 @@
<#
.SYNOPSIS
Validates that a Monaco Editor update was performed correctly.
.DESCRIPTION
Runs a series of checks against the Monaco Editor files in the repository
to ensure the update is valid and no regressions were introduced.
Tests:
- loader.js exists and contains version info
- monaco_languages.json is valid JSON with expected structure
- All expected built-in Monaco languages are present
- All PowerToys custom languages are registered
- Custom language extension mappings are present
- Monaco directory structure is intact
- No empty/corrupt core files
- Version consistency across Monaco files
.PARAMETER RepoRoot
The root of the PowerToys repository. Defaults to the repo root
relative to this script.
.EXAMPLE
./validate-monaco-update.tests.ps1
./validate-monaco-update.tests.ps1 -RepoRoot "C:\src\PowerToys"
#>
[CmdletBinding()]
param(
[Parameter()]
[string]$RepoRoot
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
if (-not $RepoRoot) {
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot ".." ".." "..")).Path
}
$monacoDir = Join-Path $RepoRoot "src" "Monaco"
$monacoSrcDir = Join-Path $monacoDir "monacoSRC"
$minDir = Join-Path $monacoSrcDir "min"
$loaderJsPath = Join-Path $minDir "vs" "loader.js"
$languagesJsonPath = Join-Path $monacoDir "monaco_languages.json"
$customLangsDir = Join-Path $monacoDir "customLanguages"
$testsPassed = 0
$testsFailed = 0
$testResults = @()
function Assert-Test {
param(
[string]$Name,
[scriptblock]$Test
)
try {
$result = & $Test
if ($result -eq $false) {
throw "Assertion returned false"
}
$script:testsPassed++
$script:testResults += [PSCustomObject]@{ Name = $Name; Status = "PASS"; Error = $null }
Write-Host " [PASS] $Name" -ForegroundColor Green
}
catch {
$script:testsFailed++
$script:testResults += [PSCustomObject]@{ Name = $Name; Status = "FAIL"; Error = $_.Exception.Message }
Write-Host " [FAIL] $Name" -ForegroundColor Red
Write-Host " $($_.Exception.Message)" -ForegroundColor Red
}
}
Write-Host "=== Monaco Editor Update Validation ===" -ForegroundColor Cyan
Write-Host "Repository root: $RepoRoot"
Write-Host ""
# ─── Test Group 1: Directory Structure ────────────────────────────────
Write-Host "--- Directory Structure ---" -ForegroundColor Yellow
Assert-Test "Monaco directory exists" {
Test-Path $monacoDir
}
Assert-Test "monacoSRC directory exists" {
Test-Path $monacoSrcDir
}
Assert-Test "min directory exists" {
Test-Path $minDir
}
Assert-Test "vs subdirectory exists" {
Test-Path (Join-Path $minDir "vs")
}
Assert-Test "editor directory exists" {
Test-Path (Join-Path $minDir "vs" "editor")
}
Assert-Test "basic-languages directory exists" {
Test-Path (Join-Path $minDir "vs" "basic-languages")
}
Assert-Test "base directory exists" {
Test-Path (Join-Path $minDir "vs" "base")
}
Assert-Test "language directory exists" {
Test-Path (Join-Path $minDir "vs" "language")
}
Assert-Test "customLanguages directory exists" {
Test-Path $customLangsDir
}
# ─── Test Group 2: Core Files ─────────────────────────────────────────
Write-Host "`n--- Core Files ---" -ForegroundColor Yellow
Assert-Test "loader.js exists" {
Test-Path $loaderJsPath
}
Assert-Test "loader.js is not empty" {
(Get-Item $loaderJsPath).Length -gt 0
}
Assert-Test "loader.js contains version string" {
$content = Get-Content $loaderJsPath -Raw
$content -match 'Version:\s*\d+\.\d+\.\d+'
}
Assert-Test "editor.main.js exists" {
Test-Path (Join-Path $minDir "vs" "editor" "editor.main.js")
}
Assert-Test "editor.main.js is not empty" {
(Get-Item (Join-Path $minDir "vs" "editor" "editor.main.js")).Length -gt 0
}
Assert-Test "editor.main.css exists" {
Test-Path (Join-Path $minDir "vs" "editor" "editor.main.css")
}
Assert-Test "monacoSpecialLanguages.js exists" {
Test-Path (Join-Path $monacoDir "monacoSpecialLanguages.js")
}
Assert-Test "generateLanguagesJson.html exists" {
Test-Path (Join-Path $monacoDir "generateLanguagesJson.html")
}
Assert-Test "index.html exists" {
Test-Path (Join-Path $monacoDir "index.html")
}
Assert-Test "customTokenThemeRules.js exists" {
Test-Path (Join-Path $monacoDir "customTokenThemeRules.js")
}
# ─── Test Group 3: monaco_languages.json ──────────────────────────────
Write-Host "`n--- monaco_languages.json ---" -ForegroundColor Yellow
Assert-Test "monaco_languages.json exists" {
Test-Path $languagesJsonPath
}
Assert-Test "monaco_languages.json is not empty" {
(Get-Item $languagesJsonPath).Length -gt 0
}
$languagesJson = $null
Assert-Test "monaco_languages.json is valid JSON" {
$script:languagesJson = Get-Content $languagesJsonPath -Raw | ConvertFrom-Json
$null -ne $script:languagesJson
}
Assert-Test "JSON has 'list' property" {
$null -ne $languagesJson.list
}
Assert-Test "Language list is a non-empty array" {
$languagesJson.list.Count -gt 0
}
Assert-Test "Minimum language count check (at least 80 languages)" {
$languagesJson.list.Count -ge 80
}
# Core built-in languages that should always be present
$expectedBuiltinLanguages = @(
"plaintext", "javascript", "typescript", "html", "css", "json",
"xml", "markdown", "yaml", "python", "java", "csharp", "cpp",
"go", "rust", "ruby", "php", "sql", "shell", "powershell",
"dockerfile", "bat", "fsharp", "lua", "r", "swift", "kotlin",
"scala", "perl", "dart", "ini", "vb"
)
$languageIds = $languagesJson.list | ForEach-Object { $_.id }
foreach ($lang in $expectedBuiltinLanguages) {
Assert-Test "Built-in language '$lang' is present" {
$lang -in $languageIds
}
}
# ─── Test Group 4: PowerToys Custom Languages ─────────────────────────
Write-Host "`n--- PowerToys Custom Languages ---" -ForegroundColor Yellow
$expectedCustomLanguages = @(
@{ Id = "reg"; Extensions = @(".reg") },
@{ Id = "gitignore"; Extensions = @(".gitignore") },
@{ Id = "srt"; Extensions = @(".srt") }
)
foreach ($custom in $expectedCustomLanguages) {
Assert-Test "Custom language '$($custom.Id)' is registered" {
$custom.Id -in $languageIds
}
foreach ($ext in $custom.Extensions) {
Assert-Test "Custom language '$($custom.Id)' has extension '$ext'" {
$lang = $languagesJson.list | Where-Object { $_.id -eq $custom.Id }
if ($null -eq $lang) { throw "Language not found" }
$ext -in $lang.extensions
}
}
}
# Custom language definition files exist
$expectedCustomFiles = @("reg.js", "gitignore.js", "srt.js")
foreach ($file in $expectedCustomFiles) {
Assert-Test "Custom language file '$file' exists" {
Test-Path (Join-Path $customLangsDir $file)
}
}
# ─── Test Group 5: Custom Language Extensions ─────────────────────────
Write-Host "`n--- Custom Language Extensions ---" -ForegroundColor Yellow
$expectedExtensions = @(
@{ Id = "cppExt"; Extensions = @(".ino", ".pde") },
@{ Id = "xmlExt"; Extensions = @(".wsdl", ".csproj", ".vcxproj", ".vbproj", ".fsproj", ".resx", ".resw") },
@{ Id = "txtExt"; Extensions = @(".sln", ".log") },
@{ Id = "razorExt"; Extensions = @(".razor") },
@{ Id = "vbExt"; Extensions = @(".vbs") },
@{ Id = "iniExt"; Extensions = @(".inf") },
@{ Id = "shellExt"; Extensions = @(".ksh", ".zsh", ".bsh") }
)
foreach ($ext in $expectedExtensions) {
Assert-Test "Extension mapping '$($ext.Id)' is registered" {
$ext.Id -in $languageIds
}
# Check all extensions from each mapping
foreach ($extension in $ext.Extensions) {
Assert-Test "Extension mapping '$($ext.Id)' has extension '$extension'" {
$lang = $languagesJson.list | Where-Object { $_.id -eq $ext.Id }
if ($null -eq $lang) { throw "Language not found" }
$extension -in $lang.extensions
}
}
}
# ─── Test Group 6: Language Entry Structure ───────────────────────────
Write-Host "`n--- Language Entry Structure ---" -ForegroundColor Yellow
Assert-Test "Every language has an 'id' field" {
$missing = @($languagesJson.list | Where-Object { -not $_.id -or $_.id.Trim() -eq "" })
$missing.Count -eq 0
}
Assert-Test "No duplicate language IDs" {
$ids = $languagesJson.list | ForEach-Object { $_.id }
$uniqueIds = $ids | Select-Object -Unique
$ids.Count -eq $uniqueIds.Count
}
Assert-Test "Languages with extensions have array-type extensions" {
$withExtensions = @($languagesJson.list | Where-Object {
($_.PSObject.Properties.Name -contains "extensions") -and ($null -ne $_.extensions)
})
$invalid = @($withExtensions | Where-Object { $_.extensions -isnot [array] })
$invalid.Count -eq 0
}
# Spot-check known extension-to-language mappings
$extensionMappings = @(
@{ Extension = ".js"; ExpectedLanguage = "javascript" },
@{ Extension = ".py"; ExpectedLanguage = "python" },
@{ Extension = ".cs"; ExpectedLanguage = "csharp" },
@{ Extension = ".html"; ExpectedLanguage = "html" },
@{ Extension = ".css"; ExpectedLanguage = "css" },
@{ Extension = ".json"; ExpectedLanguage = "json" },
@{ Extension = ".ts"; ExpectedLanguage = "typescript" },
@{ Extension = ".yaml"; ExpectedLanguage = "yaml" },
@{ Extension = ".go"; ExpectedLanguage = "go" },
@{ Extension = ".rs"; ExpectedLanguage = "rust" }
)
foreach ($mapping in $extensionMappings) {
Assert-Test "Extension '$($mapping.Extension)' maps to '$($mapping.ExpectedLanguage)'" {
$lang = $languagesJson.list | Where-Object { $_.id -eq $mapping.ExpectedLanguage }
if ($null -eq $lang) { throw "Language '$($mapping.ExpectedLanguage)' not found" }
$mapping.Extension -in $lang.extensions
}
}
# ─── Test Group 7: Version Consistency ────────────────────────────────
Write-Host "`n--- Version Consistency ---" -ForegroundColor Yellow
Assert-Test "All NLS files in editor directory have matching versions" {
$editorDir = Join-Path $minDir "vs" "editor"
$nlsFiles = Get-ChildItem -Path $editorDir -Filter "editor.main.nls*.js" -ErrorAction SilentlyContinue
if ($nlsFiles.Count -eq 0) { throw "No NLS files found" }
$loaderContent = Get-Content $loaderJsPath -Raw
$null = $loaderContent -match 'Version:\s*(\d+\.\d+\.\d+)'
$loaderVersion = $Matches[1]
foreach ($nlsFile in $nlsFiles) {
$content = Get-Content $nlsFile.FullName -Raw -ErrorAction SilentlyContinue
if ($content -and $content -match 'Version:\s*(\d+\.\d+\.\d+)') {
if ($Matches[1] -ne $loaderVersion) {
throw "Version mismatch in $($nlsFile.Name): expected $loaderVersion, found $($Matches[1])"
}
}
}
$true
}
Assert-Test "basic-languages files have matching versions" {
$loaderContent = Get-Content $loaderJsPath -Raw
$null = $loaderContent -match 'Version:\s*(\d+\.\d+\.\d+)'
$loaderVersion = $Matches[1]
$basicLangsDir = Join-Path $minDir "vs" "basic-languages"
# Spot-check a few language files
$checkFiles = @("javascript/javascript.js", "python/python.js", "html/html.js")
foreach ($file in $checkFiles) {
$filePath = Join-Path $basicLangsDir $file
if (Test-Path $filePath) {
$content = Get-Content $filePath -Raw
if ($content -match 'Version:\s*(\d+\.\d+\.\d+)') {
if ($Matches[1] -ne $loaderVersion) {
throw "Version mismatch in $file: expected $loaderVersion, found $($Matches[1])"
}
}
}
}
$true
}
# ─── Summary ──────────────────────────────────────────────────────────
Write-Host ""
Write-Host "=== Test Summary ===" -ForegroundColor Cyan
Write-Host "Passed: $testsPassed" -ForegroundColor Green
Write-Host "Failed: $testsFailed" -ForegroundColor $(if ($testsFailed -gt 0) { "Red" } else { "Green" })
Write-Host "Total: $($testsPassed + $testsFailed)"
if ($testsFailed -gt 0) {
Write-Host "`nFailed tests:" -ForegroundColor Red
$testResults | Where-Object { $_.Status -eq "FAIL" } | ForEach-Object {
Write-Host " - $($_.Name): $($_.Error)" -ForegroundColor Red
}
exit 1
}
Write-Host "`nAll tests passed!" -ForegroundColor Green
exit 0