Compare commits

...

2 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
4edef48018 fix: add Monaco/Puppeteer terms to spell-check allow list
Agent-Logs-Url: https://github.com/microsoft/PowerToys/sessions/755156df-f3b9-43b8-b696-94b18a48c257

Co-authored-by: crutkas <1462282+crutkas@users.noreply.github.com>
2026-04-01 05:10:50 +00:00
Clint Rutkas
ef53ea360f 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>
2026-03-31 21:30:48 -07:00
5 changed files with 909 additions and 0 deletions

View File

@@ -367,3 +367,14 @@ Nonpaged
# XAML
Untargeted
# Monaco Editor / Puppeteer
cdp
Cdp
crdownload
networkidle
# Monaco Editor languages
kotlin
ksh
pde

View File

@@ -0,0 +1,230 @@
/**
* generate-monaco-languages.js
*
* Generates monaco_languages.json using Puppeteer to run the existing
* generateLanguagesJson.html in a headless browser. This exactly mirrors
* the manual process described in doc/devdocs/common/FilePreviewCommon.md.
*
* Usage: node generate-monaco-languages.js <path-to-src/Monaco>
*/
"use strict";
const path = require("path");
const fs = require("fs");
const http = require("http");
const monacoDir = process.argv[2];
if (!monacoDir) {
console.error("Usage: node generate-monaco-languages.js <monaco-dir>");
process.exit(1);
}
const absMonacoDir = path.resolve(monacoDir);
const outputPath = path.join(absMonacoDir, "monaco_languages.json");
const htmlPath = path.join(absMonacoDir, "generateLanguagesJson.html");
if (!fs.existsSync(htmlPath)) {
console.error(`generateLanguagesJson.html not found at: ${htmlPath}`);
process.exit(1);
}
async function main() {
let server;
let browser;
try {
// Step 1: Start a local HTTP server serving the Monaco directory.
// The generateLanguagesJson.html must be served over HTTP because
// browsers block ES module imports and AMD require from file:// URLs.
server = await startServer(absMonacoDir);
const port = server.address().port;
console.log(`Local server started on port ${port}`);
// Step 2: Launch headless browser via Puppeteer
const puppeteer = require("puppeteer");
browser = await puppeteer.launch({
headless: true,
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const page = await browser.newPage();
// Step 3: Set up download interception.
// generateLanguagesJson.html creates an <a> element and clicks it to
// trigger a download of monaco_languages.json. We intercept this
// using CDP to redirect downloads to a temp directory.
const downloadDir = path.join(
absMonacoDir,
".monaco-download-tmp"
);
fs.mkdirSync(downloadDir, { recursive: true });
try {
const cdp = await browser.target().createCDPSession();
await cdp.send("Browser.setDownloadBehavior", {
behavior: "allow",
downloadPath: downloadDir,
});
const pageCdp = await page.createCDPSession();
await pageCdp.send("Page.setDownloadBehavior", {
behavior: "allow",
downloadPath: downloadDir,
});
} catch (err) {
throw new Error(
`Failed to configure download behavior via CDP: ${err.message}`
);
}
// Step 4: Navigate to the generator page.
// The page auto-loads Monaco, registers custom languages, calls
// getLanguages(), and triggers a download of the JSON.
console.log("Navigating to generateLanguagesJson.html...");
await page.goto(`http://localhost:${port}/generateLanguagesJson.html`, {
waitUntil: "networkidle0",
timeout: 60000,
});
// Step 5: Wait for the download to complete.
const downloadedFile = await waitForDownload(
downloadDir,
"monaco_languages.json",
30000
);
// Step 6: Move the downloaded file to the target location.
const downloadedContent = fs.readFileSync(downloadedFile, "utf-8");
// Validate the content is valid JSON before writing
const parsed = JSON.parse(downloadedContent);
if (!parsed.list || !Array.isArray(parsed.list)) {
throw new Error(
"Downloaded JSON does not have the expected { list: [...] } structure"
);
}
fs.writeFileSync(outputPath, downloadedContent, "utf-8");
console.log(
`monaco_languages.json written with ${parsed.list.length} languages.`
);
} catch (err) {
console.error("Failed to generate monaco_languages.json:", err.message);
process.exit(1);
} finally {
if (browser) {
await browser.close().catch(() => {});
}
if (server) {
server.close();
}
// Clean up temp download directory AFTER browser is closed
const downloadDir = path.join(absMonacoDir, ".monaco-download-tmp");
if (fs.existsSync(downloadDir)) {
fs.rmSync(downloadDir, { recursive: true, force: true });
}
}
}
/**
* Starts a simple HTTP server that serves static files from the given
* directory. Supports .js, .html, .css, .json, .ttf MIME types.
*/
function startServer(rootDir) {
return new Promise((resolve, reject) => {
const mimeTypes = {
".html": "text/html",
".js": "application/javascript",
".mjs": "application/javascript",
".css": "text/css",
".json": "application/json",
".ttf": "font/ttf",
".woff": "font/woff",
".woff2": "font/woff2",
".svg": "image/svg+xml",
".png": "image/png",
};
const server = http.createServer((req, res) => {
const urlPath = decodeURIComponent(req.url.split("?")[0]);
const filePath = path.join(rootDir, urlPath);
// Security: ensure we don't serve files outside rootDir
const resolvedRoot = path.resolve(rootDir);
const resolvedPath = path.resolve(rootDir, urlPath);
if (
resolvedPath !== resolvedRoot &&
!resolvedPath.startsWith(resolvedRoot + path.sep)
) {
res.writeHead(403);
res.end("Forbidden");
return;
}
if (!fs.existsSync(resolvedPath) || fs.statSync(resolvedPath).isDirectory()) {
res.writeHead(404);
res.end("Not Found");
return;
}
const ext = path.extname(resolvedPath).toLowerCase();
const contentType = mimeTypes[ext] || "application/octet-stream";
const content = fs.readFileSync(resolvedPath);
res.writeHead(200, { "Content-Type": contentType });
res.end(content);
});
server.listen(0, "127.0.0.1", () => {
resolve(server);
});
server.on("error", reject);
});
}
/**
* Waits for a file to appear in the download directory.
* Puppeteer downloads may have a .crdownload suffix while in progress.
*/
function waitForDownload(downloadDir, expectedFilename, timeoutMs) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = () => {
const files = fs.readdirSync(downloadDir);
// Check for the expected file (not a .crdownload partial)
const targetFile = files.find(
(f) => f === expectedFilename && !f.endsWith(".crdownload")
);
if (targetFile) {
const filePath = path.join(downloadDir, targetFile);
// Ensure file has content (not still being written)
const stat = fs.statSync(filePath);
if (stat.size > 0) {
resolve(filePath);
return;
}
}
if (Date.now() - startTime > timeoutMs) {
reject(
new Error(
`Timed out waiting for ${expectedFilename} download after ${timeoutMs}ms. ` +
`Files in download dir: ${files.join(", ") || "(empty)"}`
)
);
return;
}
setTimeout(check, 500);
};
check();
});
}
main();

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

162
.github/scripts/update-monaco-editor.ps1 vendored Normal file
View File

@@ -0,0 +1,162 @@
<#
.SYNOPSIS
Updates the Monaco Editor in PowerToys to the latest (or specified) version.
.DESCRIPTION
This script automates the Monaco Editor update process described in
doc/devdocs/common/FilePreviewCommon.md:
1. Downloads Monaco editor via npm
2. Copies the min folder into src/Monaco/monacoSRC/
3. Generates the monaco_languages.json file using Puppeteer (headless browser)
.PARAMETER Version
The Monaco Editor npm version to install. Defaults to "latest".
.PARAMETER RepoRoot
The root of the PowerToys repository. Defaults to the repo root relative to this script.
.EXAMPLE
./update-monaco-editor.ps1
./update-monaco-editor.ps1 -Version "0.55.1"
#>
[CmdletBinding()]
param(
[Parameter()]
[string]$Version = "latest",
[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"
$languagesJsonPath = Join-Path $monacoDir "monaco_languages.json"
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "monaco-update-$([System.Guid]::NewGuid().ToString('N').Substring(0, 8))"
Write-Host "=== Monaco Editor Update Script ==="
Write-Host "Repository root: $RepoRoot"
Write-Host "Target version: $Version"
Write-Host "Temp directory: $tempDir"
# Verify prerequisites
$npmPath = Get-Command npm -ErrorAction SilentlyContinue
if (-not $npmPath) {
throw "npm is required but not found in PATH. Please install Node.js."
}
$nodePath = Get-Command node -ErrorAction SilentlyContinue
if (-not $nodePath) {
throw "node is required but not found in PATH. Please install Node.js."
}
# Verify repo structure
if (-not (Test-Path $monacoDir)) {
throw "Monaco directory not found at: $monacoDir"
}
try {
# Step 1: Download Monaco via npm
Write-Host "`n--- Step 1: Downloading Monaco Editor ($Version) via npm ---"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
Push-Location $tempDir
try {
$versionSpec = if ($Version -eq "latest") { "monaco-editor@latest" } else { "monaco-editor@$Version" }
npm init -y 2>&1 | Out-Null
npm install $versionSpec 2>&1
if ($LASTEXITCODE -ne 0) {
throw "npm install failed with exit code $LASTEXITCODE"
}
}
finally {
Pop-Location
}
$downloadedMinDir = Join-Path $tempDir "node_modules" "monaco-editor" "min"
if (-not (Test-Path $downloadedMinDir)) {
throw "Downloaded Monaco min directory not found at: $downloadedMinDir"
}
# Detect the downloaded version from loader.js
$loaderJsPath = Join-Path $downloadedMinDir "vs" "loader.js"
if (-not (Test-Path $loaderJsPath)) {
throw "loader.js not found in downloaded Monaco package"
}
$loaderContent = Get-Content $loaderJsPath -Raw
if ($loaderContent -match 'Version:\s*(\d+\.\d+\.\d+)') {
$newVersion = $Matches[1]
Write-Host "Downloaded Monaco version: $newVersion"
}
else {
Write-Warning "Could not detect version from loader.js"
$newVersion = $Version
}
# Step 2: Replace monacoSRC/min folder
Write-Host "`n--- Step 2: Replacing monacoSRC/min with new version ---"
$targetMinDir = Join-Path $monacoSrcDir "min"
if (Test-Path $targetMinDir) {
Write-Host "Removing existing min directory..."
Remove-Item -Recurse -Force $targetMinDir
}
Write-Host "Copying new min directory..."
Copy-Item -Recurse -Force $downloadedMinDir $targetMinDir
# Step 3: Generate monaco_languages.json using Puppeteer
Write-Host "`n--- Step 3: Generating monaco_languages.json (Puppeteer) ---"
# Install Puppeteer in the temp directory
Push-Location $tempDir
try {
Write-Host "Installing Puppeteer..."
npm install puppeteer 2>&1
if ($LASTEXITCODE -ne 0) {
throw "npm install puppeteer failed with exit code $LASTEXITCODE"
}
}
finally {
Pop-Location
}
$generateScript = Join-Path $PSScriptRoot "generate-monaco-languages.js"
# Set NODE_PATH so the generate script can find puppeteer from the temp dir
$env:NODE_PATH = Join-Path $tempDir "node_modules"
node $generateScript $monacoDir
if ($LASTEXITCODE -ne 0) {
throw "Failed to generate monaco_languages.json (exit code: $LASTEXITCODE)"
}
if (-not (Test-Path $languagesJsonPath)) {
throw "monaco_languages.json was not generated at: $languagesJsonPath"
}
Write-Host "`n=== Monaco Editor update complete ==="
Write-Host "Updated to version: $newVersion"
Write-Host "Languages JSON: $languagesJsonPath"
# Output the new version for the workflow to use
Write-Output "MONACO_VERSION=$newVersion"
}
finally {
# Clean up temp directory
if (Test-Path $tempDir) {
Remove-Item -Recurse -Force $tempDir -ErrorAction SilentlyContinue
}
# Remove NODE_PATH override
Remove-Item Env:\NODE_PATH -ErrorAction SilentlyContinue
}

View File

@@ -0,0 +1,135 @@
# Update Monaco Editor
#
# Automates the Monaco Editor update process described in
# doc/devdocs/common/FilePreviewCommon.md:
# 1. Downloads the latest (or specified) Monaco Editor from npm
# 2. Replaces src/Monaco/monacoSRC/min with the new version
# 3. Regenerates monaco_languages.json via Puppeteer (headless browser)
# 4. Runs validation tests
# 5. Creates a pull request with the changes
#
# Trigger manually via workflow_dispatch.
# Uncomment the schedule block below to enable weekly automatic checks.
name: Update Monaco Editor
on:
workflow_dispatch:
inputs:
version:
description: 'Monaco Editor version (e.g. "0.55.1"). Leave empty for latest.'
required: false
default: ''
type: string
# Uncomment the following to enable weekly automatic update checks:
# schedule:
# - cron: '0 9 * * 1' # Every Monday at 9:00 UTC
permissions:
contents: write
pull-requests: write
jobs:
update-monaco:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Get current Monaco version
id: current_version
shell: bash
run: |
CURRENT=$(grep -oP 'Version:\s*\K[\d.]+' src/Monaco/monacoSRC/min/vs/loader.js || echo "unknown")
echo "version=$CURRENT" >> "$GITHUB_OUTPUT"
echo "Current Monaco version: $CURRENT"
- name: Run Monaco update script
id: update
shell: pwsh
run: |
$version = '${{ inputs.version }}'
if ([string]::IsNullOrWhiteSpace($version)) {
$version = 'latest'
}
$output = & ./.github/scripts/update-monaco-editor.ps1 -Version $version -RepoRoot $env:GITHUB_WORKSPACE
# Extract version from script output
$versionLine = $output | Select-String -Pattern '^MONACO_VERSION=' | Select-Object -First 1
if ($versionLine) {
$newVersion = $versionLine.ToString().Split('=')[1]
echo "new_version=$newVersion" >> $env:GITHUB_OUTPUT
Write-Host "New Monaco version: $newVersion"
}
- name: Run validation tests
shell: pwsh
run: |
./.github/scripts/tests/validate-monaco-update.tests.ps1 -RepoRoot $env:GITHUB_WORKSPACE
- name: Check for changes
id: changes
shell: bash
run: |
if git diff --quiet; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
echo "No changes detected - Monaco may already be up to date."
else
echo "has_changes=true" >> "$GITHUB_OUTPUT"
CHANGED_FILES=$(git diff --stat | tail -1)
echo "changed_files=$CHANGED_FILES" >> "$GITHUB_OUTPUT"
fi
- name: Create pull request
if: steps.changes.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "Update Monaco Editor to ${{ steps.update.outputs.new_version }}"
title: "Update Monaco Editor from ${{ steps.current_version.outputs.version }} to ${{ steps.update.outputs.new_version }}"
body: |
## Summary
Automated update of the Monaco Editor dependency.
**Previous version:** ${{ steps.current_version.outputs.version }}
**New version:** ${{ steps.update.outputs.new_version }}
## Changes
- Updated `src/Monaco/monacoSRC/min/` with the new Monaco Editor release
- Regenerated `src/Monaco/monaco_languages.json`
## Validation
The following automated checks passed:
- ✅ `loader.js` contains valid version header
- ✅ Directory structure intact (`vs/editor/`, `vs/basic-languages/`, `vs/base/`, `vs/language/`)
- ✅ `monaco_languages.json` is valid JSON with expected structure
- ✅ All expected built-in languages are present (32+ core languages verified)
- ✅ All PowerToys custom languages registered (`reg`, `gitignore`, `srt`)
- ✅ All custom extension mappings present (`cppExt`, `xmlExt`, `txtExt`, etc.)
- ✅ No duplicate language IDs
- ✅ Version consistency across Monaco files
- ✅ Extension-to-language mappings verified (`.js`→javascript, `.py`→python, etc.)
## Manual Verification
Before merging, please verify:
- [ ] File Explorer Dev File Preview works correctly
- [ ] Peek module previews code files properly
- [ ] Registry Preview module functions normally
## Reference
- [Monaco Editor update docs](doc/devdocs/common/FilePreviewCommon.md#update-monaco-editor)
- [Monaco Editor releases](https://github.com/microsoft/monaco-editor/releases)
branch: automated/update-monaco-editor
delete-branch: true
labels: |
dependencies