mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-04 01:20:02 +02:00
Compare commits
2 Commits
powerscrip
...
crutkas/au
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4edef48018 | ||
|
|
ef53ea360f |
11
.github/actions/spell-check/allow/code.txt
vendored
11
.github/actions/spell-check/allow/code.txt
vendored
@@ -367,3 +367,14 @@ Nonpaged
|
||||
|
||||
# XAML
|
||||
Untargeted
|
||||
|
||||
# Monaco Editor / Puppeteer
|
||||
cdp
|
||||
Cdp
|
||||
crdownload
|
||||
networkidle
|
||||
|
||||
# Monaco Editor languages
|
||||
kotlin
|
||||
ksh
|
||||
pde
|
||||
|
||||
230
.github/scripts/generate-monaco-languages.js
vendored
Normal file
230
.github/scripts/generate-monaco-languages.js
vendored
Normal 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();
|
||||
371
.github/scripts/tests/validate-monaco-update.tests.ps1
vendored
Normal file
371
.github/scripts/tests/validate-monaco-update.tests.ps1
vendored
Normal 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
162
.github/scripts/update-monaco-editor.ps1
vendored
Normal 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
|
||||
}
|
||||
135
.github/workflows/update-monaco-editor.yml
vendored
Normal file
135
.github/workflows/update-monaco-editor.yml
vendored
Normal 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
|
||||
Reference in New Issue
Block a user