microsoft/hve-core
Publicmirrored fromhttps://github.com/microsoft/hve-coreAvailable
scripts/extension/Package-Extension.ps1
838lines ยท modecode
| 1 | #!/usr/bin/env pwsh |
| 2 | # Copyright (c) Microsoft Corporation. |
| 3 | # SPDX-License-Identifier: MIT |
| 4 | #Requires -Version 7.0 |
| 5 | |
| 6 | <# |
| 7 | .SYNOPSIS |
| 8 | Packages the HVE Core VS Code extension. |
| 9 | |
| 10 | .DESCRIPTION |
| 11 | This script packages the VS Code extension into a .vsix file. |
| 12 | It uses the version from package.json or a specified version. |
| 13 | Optionally adds a dev patch number for pre-release builds. |
| 14 | Supports VS Code Marketplace pre-release channel with -PreRelease switch. |
| 15 | |
| 16 | .PARAMETER Version |
| 17 | Optional. The version to use for the package. |
| 18 | If not specified, uses the version from package.json. |
| 19 | |
| 20 | .PARAMETER DevPatchNumber |
| 21 | Optional. Dev patch number to append (e.g., "123" creates "1.0.0-dev.123"). |
| 22 | |
| 23 | .PARAMETER ChangelogPath |
| 24 | Optional. Path to a changelog file to include in the package. |
| 25 | |
| 26 | .PARAMETER PreRelease |
| 27 | Optional. When specified, packages the extension for VS Code Marketplace pre-release channel. |
| 28 | Uses vsce --pre-release flag which marks the extension for the pre-release track. |
| 29 | |
| 30 | .EXAMPLE |
| 31 | ./Package-Extension.ps1 |
| 32 | # Packages using version from package.json |
| 33 | |
| 34 | .EXAMPLE |
| 35 | ./Package-Extension.ps1 -Version "2.0.0" |
| 36 | # Packages with specific version |
| 37 | |
| 38 | .EXAMPLE |
| 39 | ./Package-Extension.ps1 -DevPatchNumber "123" |
| 40 | # Packages with dev version (e.g., 1.0.0-dev.123) |
| 41 | |
| 42 | .EXAMPLE |
| 43 | ./Package-Extension.ps1 -Version "1.1.0" -DevPatchNumber "456" |
| 44 | # Packages with specific dev version (1.1.0-dev.456) |
| 45 | |
| 46 | .EXAMPLE |
| 47 | ./Package-Extension.ps1 -PreRelease |
| 48 | # Packages for VS Code Marketplace pre-release channel |
| 49 | |
| 50 | .EXAMPLE |
| 51 | ./Package-Extension.ps1 -Version "1.1.0" -PreRelease |
| 52 | # Packages with ODD minor version for pre-release channel |
| 53 | |
| 54 | .EXAMPLE |
| 55 | . ./Package-Extension.ps1 |
| 56 | # Dot-source to import functions for testing without executing packaging. |
| 57 | #> |
| 58 | |
| 59 | [CmdletBinding()] |
| 60 | param( |
| 61 | [Parameter(Mandatory = $false)] |
| 62 | [string]$Version = "", |
| 63 | |
| 64 | [Parameter(Mandatory = $false)] |
| 65 | [string]$DevPatchNumber = "", |
| 66 | |
| 67 | [Parameter(Mandatory = $false)] |
| 68 | [string]$ChangelogPath = "", |
| 69 | |
| 70 | [Parameter(Mandatory = $false)] |
| 71 | [switch]$PreRelease |
| 72 | ) |
| 73 | |
| 74 | $ErrorActionPreference = 'Stop' |
| 75 | |
| 76 | Import-Module (Join-Path $PSScriptRoot "../lib/Modules/CIHelpers.psm1") -Force |
| 77 | |
| 78 | #region Pure Functions |
| 79 | |
| 80 | function Test-VsceAvailable { |
| 81 | <# |
| 82 | .SYNOPSIS |
| 83 | Checks if vsce or npx is available for packaging. |
| 84 | .OUTPUTS |
| 85 | Hashtable with IsAvailable, CommandType ('vsce', 'npx', or $null), and Command path. |
| 86 | #> |
| 87 | [CmdletBinding()] |
| 88 | [OutputType([hashtable])] |
| 89 | param() |
| 90 | |
| 91 | $vsceCmd = Get-Command vsce -ErrorAction SilentlyContinue |
| 92 | if ($vsceCmd) { |
| 93 | return @{ |
| 94 | IsAvailable = $true |
| 95 | CommandType = 'vsce' |
| 96 | Command = $vsceCmd.Source |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | $npxCmd = Get-Command npx -ErrorAction SilentlyContinue |
| 101 | if ($npxCmd) { |
| 102 | return @{ |
| 103 | IsAvailable = $true |
| 104 | CommandType = 'npx' |
| 105 | Command = $npxCmd.Source |
| 106 | } |
| 107 | } |
| 108 | |
| 109 | return @{ |
| 110 | IsAvailable = $false |
| 111 | CommandType = $null |
| 112 | Command = $null |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | function Get-ExtensionOutputPath { |
| 117 | <# |
| 118 | .SYNOPSIS |
| 119 | Constructs the expected .vsix output path from extension directory and version. |
| 120 | .PARAMETER ExtensionDirectory |
| 121 | The path to the extension directory. |
| 122 | .PARAMETER ExtensionName |
| 123 | The name of the extension (from package.json). |
| 124 | .PARAMETER PackageVersion |
| 125 | The version string to use in the filename. |
| 126 | .OUTPUTS |
| 127 | String path to the expected .vsix file. |
| 128 | #> |
| 129 | [CmdletBinding()] |
| 130 | [OutputType([string])] |
| 131 | param( |
| 132 | [Parameter(Mandatory = $true)] |
| 133 | [string]$ExtensionDirectory, |
| 134 | |
| 135 | [Parameter(Mandatory = $true)] |
| 136 | [string]$ExtensionName, |
| 137 | |
| 138 | [Parameter(Mandatory = $true)] |
| 139 | [string]$PackageVersion |
| 140 | ) |
| 141 | |
| 142 | $vsixFileName = "$ExtensionName-$PackageVersion.vsix" |
| 143 | return Join-Path $ExtensionDirectory $vsixFileName |
| 144 | } |
| 145 | |
| 146 | function Test-ExtensionManifestValid { |
| 147 | <# |
| 148 | .SYNOPSIS |
| 149 | Validates an extension manifest (package.json content) for required fields and format. |
| 150 | .PARAMETER ManifestContent |
| 151 | The parsed package.json content as a PSObject. |
| 152 | .OUTPUTS |
| 153 | Hashtable with IsValid boolean and Errors array. |
| 154 | #> |
| 155 | [CmdletBinding()] |
| 156 | [OutputType([hashtable])] |
| 157 | param( |
| 158 | [Parameter(Mandatory = $true)] |
| 159 | [PSObject]$ManifestContent |
| 160 | ) |
| 161 | |
| 162 | $errors = @() |
| 163 | |
| 164 | # Check required fields |
| 165 | if (-not $ManifestContent.PSObject.Properties['name']) { |
| 166 | $errors += "Missing required 'name' field" |
| 167 | } |
| 168 | |
| 169 | if (-not $ManifestContent.PSObject.Properties['version']) { |
| 170 | $errors += "Missing required 'version' field" |
| 171 | } elseif ($ManifestContent.version -notmatch '^\d+\.\d+\.\d+') { |
| 172 | $errors += "Invalid version format: '$($ManifestContent.version)'. Expected semantic version (e.g., 1.0.0)" |
| 173 | } |
| 174 | |
| 175 | if (-not $ManifestContent.PSObject.Properties['publisher']) { |
| 176 | $errors += "Missing required 'publisher' field" |
| 177 | } |
| 178 | |
| 179 | if (-not $ManifestContent.PSObject.Properties['engines']) { |
| 180 | $errors += "Missing required 'engines' field" |
| 181 | } elseif (-not $ManifestContent.engines.PSObject.Properties['vscode']) { |
| 182 | $errors += "Missing required 'engines.vscode' field" |
| 183 | } |
| 184 | |
| 185 | return @{ |
| 186 | IsValid = ($errors.Count -eq 0) |
| 187 | Errors = $errors |
| 188 | } |
| 189 | } |
| 190 | |
| 191 | function Get-VscePackageCommand { |
| 192 | <# |
| 193 | .SYNOPSIS |
| 194 | Builds the vsce package command arguments without executing. |
| 195 | .PARAMETER CommandType |
| 196 | The type of command to use ('vsce' or 'npx'). |
| 197 | .PARAMETER PreRelease |
| 198 | Whether to include the --pre-release flag. |
| 199 | .OUTPUTS |
| 200 | Hashtable with Executable and Arguments array. |
| 201 | #> |
| 202 | [CmdletBinding()] |
| 203 | [OutputType([hashtable])] |
| 204 | param( |
| 205 | [Parameter(Mandatory = $true)] |
| 206 | [ValidateSet('vsce', 'npx')] |
| 207 | [string]$CommandType, |
| 208 | |
| 209 | [Parameter(Mandatory = $false)] |
| 210 | [switch]$PreRelease |
| 211 | ) |
| 212 | |
| 213 | $vsceArgs = @('package', '--no-dependencies') |
| 214 | if ($PreRelease) { |
| 215 | $vsceArgs += '--pre-release' |
| 216 | } |
| 217 | |
| 218 | if ($CommandType -eq 'npx') { |
| 219 | # --yes auto-confirms npx package installation for non-interactive CI environments |
| 220 | return @{ |
| 221 | Executable = 'npx' |
| 222 | Arguments = @('--yes', '@vscode/vsce') + $vsceArgs |
| 223 | } |
| 224 | } |
| 225 | |
| 226 | return @{ |
| 227 | Executable = 'vsce' |
| 228 | Arguments = $vsceArgs |
| 229 | } |
| 230 | } |
| 231 | |
| 232 | function New-PackagingResult { |
| 233 | <# |
| 234 | .SYNOPSIS |
| 235 | Creates a standardized packaging result object. |
| 236 | .PARAMETER Success |
| 237 | Whether the packaging operation succeeded. |
| 238 | .PARAMETER OutputPath |
| 239 | Path to the generated .vsix file (if successful). |
| 240 | .PARAMETER Version |
| 241 | The package version used. |
| 242 | .PARAMETER ErrorMessage |
| 243 | Error message if the operation failed. |
| 244 | .OUTPUTS |
| 245 | Hashtable with Success, OutputPath, Version, and ErrorMessage. |
| 246 | #> |
| 247 | [CmdletBinding()] |
| 248 | [OutputType([hashtable])] |
| 249 | param( |
| 250 | [Parameter(Mandatory = $true)] |
| 251 | [bool]$Success, |
| 252 | |
| 253 | [Parameter(Mandatory = $false)] |
| 254 | [string]$OutputPath = "", |
| 255 | |
| 256 | [Parameter(Mandatory = $false)] |
| 257 | [string]$Version = "", |
| 258 | |
| 259 | [Parameter(Mandatory = $false)] |
| 260 | [string]$ErrorMessage = "" |
| 261 | ) |
| 262 | |
| 263 | return @{ |
| 264 | Success = $Success |
| 265 | OutputPath = $OutputPath |
| 266 | Version = $Version |
| 267 | ErrorMessage = $ErrorMessage |
| 268 | } |
| 269 | } |
| 270 | |
| 271 | function Get-ResolvedPackageVersion { |
| 272 | <# |
| 273 | .SYNOPSIS |
| 274 | Resolves the package version from parameters or manifest content. |
| 275 | .PARAMETER SpecifiedVersion |
| 276 | Version specified via parameter (may be empty). |
| 277 | .PARAMETER ManifestVersion |
| 278 | Version from the package.json manifest. |
| 279 | .PARAMETER DevPatchNumber |
| 280 | Optional dev patch number to append. |
| 281 | .OUTPUTS |
| 282 | Hashtable with IsValid, BaseVersion, PackageVersion, and ErrorMessage. |
| 283 | #> |
| 284 | [CmdletBinding()] |
| 285 | [OutputType([hashtable])] |
| 286 | param( |
| 287 | [Parameter(Mandatory = $false)] |
| 288 | [string]$SpecifiedVersion = "", |
| 289 | |
| 290 | [Parameter(Mandatory = $true)] |
| 291 | [string]$ManifestVersion, |
| 292 | |
| 293 | [Parameter(Mandatory = $false)] |
| 294 | [string]$DevPatchNumber = "" |
| 295 | ) |
| 296 | |
| 297 | $baseVersion = "" |
| 298 | |
| 299 | if ($SpecifiedVersion -and $SpecifiedVersion -ne "") { |
| 300 | # Validate specified version format |
| 301 | if ($SpecifiedVersion -notmatch '^\d+\.\d+\.\d+$') { |
| 302 | return @{ |
| 303 | IsValid = $false |
| 304 | BaseVersion = "" |
| 305 | PackageVersion = "" |
| 306 | ErrorMessage = "Invalid version format specified: '$SpecifiedVersion'. Expected semantic version format (e.g., 1.0.0)." |
| 307 | } |
| 308 | } |
| 309 | $baseVersion = $SpecifiedVersion |
| 310 | } else { |
| 311 | # Validate manifest version |
| 312 | if ($ManifestVersion -notmatch '^\d+\.\d+\.\d+') { |
| 313 | return @{ |
| 314 | IsValid = $false |
| 315 | BaseVersion = "" |
| 316 | PackageVersion = "" |
| 317 | ErrorMessage = "Invalid version format in package.json: '$ManifestVersion'. Expected semantic version format (e.g., 1.0.0)." |
| 318 | } |
| 319 | } |
| 320 | # Extract base version |
| 321 | $ManifestVersion -match '^(\d+\.\d+\.\d+)' | Out-Null |
| 322 | $baseVersion = $Matches[1] |
| 323 | } |
| 324 | |
| 325 | # Apply dev patch number if provided |
| 326 | $packageVersion = if ($DevPatchNumber -and $DevPatchNumber -ne "") { |
| 327 | "$baseVersion-dev.$DevPatchNumber" |
| 328 | } else { |
| 329 | $baseVersion |
| 330 | } |
| 331 | |
| 332 | return @{ |
| 333 | IsValid = $true |
| 334 | BaseVersion = $baseVersion |
| 335 | PackageVersion = $packageVersion |
| 336 | ErrorMessage = "" |
| 337 | } |
| 338 | } |
| 339 | |
| 340 | function Test-PackagingInputsValid { |
| 341 | <# |
| 342 | .SYNOPSIS |
| 343 | Validates all required paths for extension packaging. |
| 344 | .DESCRIPTION |
| 345 | Pure function that checks existence of ExtensionDirectory, package.json, |
| 346 | .github directory, and CIHelpers.psm1 module. Returns resolved paths for use |
| 347 | by downstream functions. |
| 348 | .PARAMETER ExtensionDirectory |
| 349 | Absolute path to the extension directory. |
| 350 | .PARAMETER RepoRoot |
| 351 | Absolute path to the repository root. |
| 352 | .OUTPUTS |
| 353 | Hashtable with IsValid, Errors array, and resolved paths. |
| 354 | #> |
| 355 | [CmdletBinding()] |
| 356 | [OutputType([hashtable])] |
| 357 | param( |
| 358 | [Parameter(Mandatory = $true)] |
| 359 | [string]$ExtensionDirectory, |
| 360 | |
| 361 | [Parameter(Mandatory = $true)] |
| 362 | [string]$RepoRoot |
| 363 | ) |
| 364 | |
| 365 | $errors = @() |
| 366 | |
| 367 | if (-not (Test-Path $ExtensionDirectory)) { |
| 368 | $errors += "Extension directory not found: $ExtensionDirectory" |
| 369 | } |
| 370 | |
| 371 | $packageJsonPath = Join-Path $ExtensionDirectory "package.json" |
| 372 | if (-not (Test-Path $packageJsonPath)) { |
| 373 | $errors += "package.json not found: $packageJsonPath" |
| 374 | } |
| 375 | |
| 376 | $githubDir = Join-Path $RepoRoot ".github" |
| 377 | if (-not (Test-Path $githubDir)) { |
| 378 | $errors += ".github directory not found: $githubDir" |
| 379 | } |
| 380 | |
| 381 | $ciHelpersPath = Join-Path $RepoRoot "scripts/lib/Modules/CIHelpers.psm1" |
| 382 | if (-not (Test-Path $ciHelpersPath)) { |
| 383 | $errors += "CIHelpers.psm1 not found: $ciHelpersPath" |
| 384 | } |
| 385 | |
| 386 | return @{ |
| 387 | IsValid = ($errors.Count -eq 0) |
| 388 | Errors = $errors |
| 389 | PackageJsonPath = $packageJsonPath |
| 390 | GitHubDir = $githubDir |
| 391 | CIHelpersPath = $ciHelpersPath |
| 392 | } |
| 393 | } |
| 394 | |
| 395 | function Get-PackagingDirectorySpec { |
| 396 | <# |
| 397 | .SYNOPSIS |
| 398 | Returns specification for directories to copy during packaging. |
| 399 | .DESCRIPTION |
| 400 | Pure function that defines source to destination mappings without performing I/O. |
| 401 | Each spec includes Source, Destination, Required flag, and optional IsFile flag. |
| 402 | .PARAMETER RepoRoot |
| 403 | Absolute path to the repository root. |
| 404 | .PARAMETER ExtensionDirectory |
| 405 | Absolute path to the extension directory. |
| 406 | .OUTPUTS |
| 407 | Array of hashtables with Source, Destination, Required, and IsFile properties. |
| 408 | #> |
| 409 | [CmdletBinding()] |
| 410 | [OutputType([hashtable[]])] |
| 411 | param( |
| 412 | [Parameter(Mandatory = $true)] |
| 413 | [string]$RepoRoot, |
| 414 | |
| 415 | [Parameter(Mandatory = $true)] |
| 416 | [string]$ExtensionDirectory |
| 417 | ) |
| 418 | |
| 419 | return @( |
| 420 | @{ |
| 421 | Source = Join-Path $RepoRoot ".github" |
| 422 | Destination = Join-Path $ExtensionDirectory ".github" |
| 423 | IsFile = $false |
| 424 | }, |
| 425 | @{ |
| 426 | Source = Join-Path $RepoRoot "scripts/dev-tools" |
| 427 | Destination = Join-Path $ExtensionDirectory "scripts/dev-tools" |
| 428 | IsFile = $false |
| 429 | }, |
| 430 | @{ |
| 431 | Source = Join-Path $RepoRoot "scripts/lib/Modules/CIHelpers.psm1" |
| 432 | Destination = Join-Path $ExtensionDirectory "scripts/lib/Modules/CIHelpers.psm1" |
| 433 | IsFile = $true |
| 434 | }, |
| 435 | @{ |
| 436 | Source = Join-Path $RepoRoot "docs/templates" |
| 437 | Destination = Join-Path $ExtensionDirectory "docs/templates" |
| 438 | IsFile = $false |
| 439 | } |
| 440 | ) |
| 441 | } |
| 442 | |
| 443 | #endregion Pure Functions |
| 444 | |
| 445 | #region I/O Functions |
| 446 | |
| 447 | function Invoke-VsceCommand { |
| 448 | <# |
| 449 | .SYNOPSIS |
| 450 | Executes vsce package command with platform-appropriate wrapper. |
| 451 | .DESCRIPTION |
| 452 | Abstracts platform-specific execution of vsce/npx commands. On Windows with npx, |
| 453 | uses cmd /c to avoid PowerShell misinterpreting @ in @vscode/vsce as splatting. |
| 454 | The UseWindowsWrapper parameter enables deterministic platform behavior in tests. |
| 455 | .PARAMETER Executable |
| 456 | The executable to run ('vsce' or 'npx'). |
| 457 | .PARAMETER Arguments |
| 458 | Array of arguments to pass to the executable. |
| 459 | .PARAMETER WorkingDirectory |
| 460 | Directory to execute the command in. |
| 461 | .PARAMETER UseWindowsWrapper |
| 462 | When true and Executable is 'npx', uses cmd /c wrapper for Windows compatibility. |
| 463 | .OUTPUTS |
| 464 | Hashtable with Success boolean and ExitCode integer. |
| 465 | #> |
| 466 | [CmdletBinding()] |
| 467 | [OutputType([hashtable])] |
| 468 | param( |
| 469 | [Parameter(Mandatory = $true)] |
| 470 | [string]$Executable, |
| 471 | |
| 472 | [Parameter(Mandatory = $true)] |
| 473 | [string[]]$Arguments, |
| 474 | |
| 475 | [Parameter(Mandatory = $true)] |
| 476 | [string]$WorkingDirectory, |
| 477 | |
| 478 | [Parameter(Mandatory = $false)] |
| 479 | [switch]$UseWindowsWrapper |
| 480 | ) |
| 481 | |
| 482 | Push-Location $WorkingDirectory |
| 483 | try { |
| 484 | $global:LASTEXITCODE = 0 |
| 485 | |
| 486 | if ($UseWindowsWrapper -and $Executable -eq 'npx') { |
| 487 | $cmdArgs = @('/c', 'npx') + $Arguments |
| 488 | & cmd @cmdArgs |
| 489 | } else { |
| 490 | & $Executable @Arguments |
| 491 | } |
| 492 | |
| 493 | return @{ |
| 494 | Success = ($LASTEXITCODE -eq 0) |
| 495 | ExitCode = $LASTEXITCODE |
| 496 | } |
| 497 | } |
| 498 | finally { |
| 499 | Pop-Location |
| 500 | } |
| 501 | } |
| 502 | |
| 503 | function Remove-PackagingArtifacts { |
| 504 | <# |
| 505 | .SYNOPSIS |
| 506 | Removes temporary directories created during packaging. |
| 507 | .DESCRIPTION |
| 508 | Cleans up directories copied to the extension folder during the packaging process. |
| 509 | Silently skips directories that do not exist. |
| 510 | .PARAMETER ExtensionDirectory |
| 511 | Absolute path to the extension directory. |
| 512 | .PARAMETER DirectoryNames |
| 513 | Array of directory names to remove. Defaults to .github, docs, scripts. |
| 514 | #> |
| 515 | [CmdletBinding()] |
| 516 | param( |
| 517 | [Parameter(Mandatory = $true)] |
| 518 | [string]$ExtensionDirectory, |
| 519 | |
| 520 | [Parameter(Mandatory = $false)] |
| 521 | [string[]]$DirectoryNames = @(".github", "docs", "scripts") |
| 522 | ) |
| 523 | |
| 524 | foreach ($dir in $DirectoryNames) { |
| 525 | $dirPath = Join-Path $ExtensionDirectory $dir |
| 526 | if (Test-Path $dirPath) { |
| 527 | Remove-Item -Path $dirPath -Recurse -Force |
| 528 | Write-Host " Removed $dir" -ForegroundColor Gray |
| 529 | } |
| 530 | } |
| 531 | } |
| 532 | |
| 533 | function Restore-PackageJsonVersion { |
| 534 | <# |
| 535 | .SYNOPSIS |
| 536 | Restores original version in package.json after packaging. |
| 537 | .DESCRIPTION |
| 538 | Writes the original version back to package.json if it was temporarily modified |
| 539 | during packaging. Safely handles null inputs by returning early. |
| 540 | .PARAMETER PackageJsonPath |
| 541 | Absolute path to the package.json file. |
| 542 | .PARAMETER PackageJson |
| 543 | The parsed package.json object to modify. |
| 544 | .PARAMETER OriginalVersion |
| 545 | The original version string to restore. |
| 546 | #> |
| 547 | [CmdletBinding()] |
| 548 | param( |
| 549 | [Parameter(Mandatory = $false)] |
| 550 | [AllowNull()] |
| 551 | [string]$PackageJsonPath, |
| 552 | |
| 553 | [Parameter(Mandatory = $false)] |
| 554 | [AllowNull()] |
| 555 | [PSObject]$PackageJson, |
| 556 | |
| 557 | [Parameter(Mandatory = $false)] |
| 558 | [AllowNull()] |
| 559 | [string]$OriginalVersion |
| 560 | ) |
| 561 | |
| 562 | # Handle null coercion: PowerShell converts $null to empty string for [string] params |
| 563 | if ([string]::IsNullOrEmpty($OriginalVersion) -or $null -eq $PackageJson -or [string]::IsNullOrEmpty($PackageJsonPath)) { |
| 564 | return |
| 565 | } |
| 566 | |
| 567 | try { |
| 568 | $PackageJson.version = $OriginalVersion |
| 569 | $PackageJson | ConvertTo-Json -Depth 10 | Set-Content -Path $PackageJsonPath -Encoding UTF8NoBOM |
| 570 | Write-Host " Version restored to: $OriginalVersion" -ForegroundColor Green |
| 571 | } |
| 572 | catch { |
| 573 | Write-Warning "Failed to restore original package.json version to '$OriginalVersion': $($_.Exception.Message)" |
| 574 | } |
| 575 | } |
| 576 | |
| 577 | #endregion I/O Functions |
| 578 | |
| 579 | #region Orchestration Functions |
| 580 | |
| 581 | function Invoke-PackageExtension { |
| 582 | <# |
| 583 | .SYNOPSIS |
| 584 | Orchestrates VS Code extension packaging with full error handling. |
| 585 | .DESCRIPTION |
| 586 | Executes the complete packaging workflow: validates paths, resolves version, |
| 587 | prepares directories, invokes vsce, and handles cleanup. |
| 588 | .PARAMETER ExtensionDirectory |
| 589 | Absolute path to the extension directory containing package.json. |
| 590 | .PARAMETER RepoRoot |
| 591 | Absolute path to the repository root directory. |
| 592 | .PARAMETER Version |
| 593 | Optional explicit version string (e.g., "1.2.3"). |
| 594 | .PARAMETER DevPatchNumber |
| 595 | Optional dev build patch number for pre-release versions. |
| 596 | .PARAMETER ChangelogPath |
| 597 | Optional path to changelog file to include in package. |
| 598 | .PARAMETER PreRelease |
| 599 | Switch to mark the package as a pre-release version. |
| 600 | .OUTPUTS |
| 601 | Hashtable with Success, OutputPath, Version, and ErrorMessage properties. |
| 602 | #> |
| 603 | [CmdletBinding()] |
| 604 | [OutputType([hashtable])] |
| 605 | param( |
| 606 | [Parameter(Mandatory = $true)] |
| 607 | [ValidateNotNullOrEmpty()] |
| 608 | [string]$ExtensionDirectory, |
| 609 | |
| 610 | [Parameter(Mandatory = $true)] |
| 611 | [ValidateNotNullOrEmpty()] |
| 612 | [string]$RepoRoot, |
| 613 | |
| 614 | [Parameter(Mandatory = $false)] |
| 615 | [string]$Version = "", |
| 616 | |
| 617 | [Parameter(Mandatory = $false)] |
| 618 | [string]$DevPatchNumber = "", |
| 619 | |
| 620 | [Parameter(Mandatory = $false)] |
| 621 | [string]$ChangelogPath = "", |
| 622 | |
| 623 | [Parameter(Mandatory = $false)] |
| 624 | [switch]$PreRelease |
| 625 | ) |
| 626 | |
| 627 | $dirsToClean = @(".github", "docs", "scripts") |
| 628 | $originalVersion = $null |
| 629 | $packageJson = $null |
| 630 | $PackageJsonPath = $null |
| 631 | $packageVersion = $null |
| 632 | $versionWasModified = $false |
| 633 | |
| 634 | try { |
| 635 | # Validate all inputs using pure function |
| 636 | $inputValidation = Test-PackagingInputsValid -ExtensionDirectory $ExtensionDirectory -RepoRoot $RepoRoot |
| 637 | if (-not $inputValidation.IsValid) { |
| 638 | return New-PackagingResult -Success $false -ErrorMessage ($inputValidation.Errors -join '; ') |
| 639 | } |
| 640 | |
| 641 | $PackageJsonPath = $inputValidation.PackageJsonPath |
| 642 | |
| 643 | Write-Host "๐ฆ HVE Core Extension Packager" -ForegroundColor Cyan |
| 644 | Write-Host "==============================" -ForegroundColor Cyan |
| 645 | Write-Host "" |
| 646 | |
| 647 | # Read and validate package.json |
| 648 | Write-Host "๐ Reading package.json..." -ForegroundColor Yellow |
| 649 | try { |
| 650 | $packageJson = Get-Content -Path $PackageJsonPath -Raw | ConvertFrom-Json |
| 651 | } |
| 652 | catch { |
| 653 | return New-PackagingResult -Success $false -ErrorMessage "Failed to parse package.json: $($_.Exception.Message)" |
| 654 | } |
| 655 | |
| 656 | $manifestValidation = Test-ExtensionManifestValid -ManifestContent $packageJson |
| 657 | if (-not $manifestValidation.IsValid) { |
| 658 | return New-PackagingResult -Success $false -ErrorMessage "Invalid package.json: $($manifestValidation.Errors -join '; ')" |
| 659 | } |
| 660 | |
| 661 | # Resolve version using pure function |
| 662 | $versionResult = Get-ResolvedPackageVersion ` |
| 663 | -SpecifiedVersion $Version ` |
| 664 | -ManifestVersion $packageJson.version ` |
| 665 | -DevPatchNumber $DevPatchNumber |
| 666 | |
| 667 | if (-not $versionResult.IsValid) { |
| 668 | return New-PackagingResult -Success $false -ErrorMessage $versionResult.ErrorMessage |
| 669 | } |
| 670 | |
| 671 | $packageVersion = $versionResult.PackageVersion |
| 672 | Write-Host " Using version: $packageVersion" -ForegroundColor Green |
| 673 | |
| 674 | # Handle temporary version update for dev builds |
| 675 | $originalVersion = $packageJson.version |
| 676 | |
| 677 | if ($packageVersion -ne $originalVersion) { |
| 678 | Write-Host "" |
| 679 | Write-Host "๐ Temporarily updating package.json version..." -ForegroundColor Yellow |
| 680 | $packageJson.version = $packageVersion |
| 681 | $packageJson | ConvertTo-Json -Depth 10 | Set-Content -Path $PackageJsonPath -Encoding UTF8NoBOM |
| 682 | Write-Host " Version: $originalVersion -> $packageVersion" -ForegroundColor Green |
| 683 | $versionWasModified = $true |
| 684 | } |
| 685 | |
| 686 | # Handle changelog if provided |
| 687 | if ($ChangelogPath -and $ChangelogPath -ne "") { |
| 688 | Write-Host "" |
| 689 | Write-Host "๐ Processing changelog..." -ForegroundColor Yellow |
| 690 | |
| 691 | if (Test-Path $ChangelogPath) { |
| 692 | $changelogDest = Join-Path $ExtensionDirectory "CHANGELOG.md" |
| 693 | Copy-Item -Path $ChangelogPath -Destination $changelogDest -Force |
| 694 | Write-Host " Copied changelog to extension directory" -ForegroundColor Green |
| 695 | } |
| 696 | else { |
| 697 | Write-Warning "Changelog file not found: $ChangelogPath" |
| 698 | } |
| 699 | } |
| 700 | |
| 701 | # Prepare extension directory |
| 702 | Write-Host "" |
| 703 | Write-Host "๐๏ธ Preparing extension directory..." -ForegroundColor Yellow |
| 704 | |
| 705 | # Clean any existing copied directories |
| 706 | foreach ($dir in $dirsToClean) { |
| 707 | $dirPath = Join-Path $ExtensionDirectory $dir |
| 708 | if (Test-Path $dirPath) { |
| 709 | Remove-Item -Path $dirPath -Recurse -Force |
| 710 | Write-Host " Cleaned existing $dir directory" -ForegroundColor Gray |
| 711 | } |
| 712 | } |
| 713 | |
| 714 | # Get and execute copy specifications |
| 715 | $copySpecs = Get-PackagingDirectorySpec -RepoRoot $RepoRoot -ExtensionDirectory $ExtensionDirectory |
| 716 | foreach ($spec in $copySpecs) { |
| 717 | $specName = Split-Path $spec.Source -Leaf |
| 718 | Write-Host " Copying $specName..." -ForegroundColor Gray |
| 719 | |
| 720 | if ($spec.IsFile) { |
| 721 | $parentDir = Split-Path $spec.Destination -Parent |
| 722 | New-Item -Path $parentDir -ItemType Directory -Force | Out-Null |
| 723 | Copy-Item -Path $spec.Source -Destination $spec.Destination -Force |
| 724 | } else { |
| 725 | $parentDir = Split-Path $spec.Destination -Parent |
| 726 | if (-not (Test-Path $parentDir)) { |
| 727 | New-Item -Path $parentDir -ItemType Directory -Force | Out-Null |
| 728 | } |
| 729 | Copy-Item -Path $spec.Source -Destination $spec.Destination -Recurse -Force |
| 730 | } |
| 731 | } |
| 732 | |
| 733 | Write-Host " โ
Extension directory prepared" -ForegroundColor Green |
| 734 | |
| 735 | # Check vsce availability using pure function |
| 736 | $vsceAvailability = Test-VsceAvailable |
| 737 | if (-not $vsceAvailability.IsAvailable) { |
| 738 | return New-PackagingResult -Success $false -ErrorMessage "Neither vsce nor npx found. Please install @vscode/vsce globally or ensure npm is available." |
| 739 | } |
| 740 | |
| 741 | # Build vsce command using pure function |
| 742 | $vsceCommand = Get-VscePackageCommand -CommandType $vsceAvailability.CommandType -PreRelease:$PreRelease |
| 743 | |
| 744 | # Package extension |
| 745 | Write-Host "" |
| 746 | Write-Host "๐ฆ Packaging extension..." -ForegroundColor Yellow |
| 747 | |
| 748 | if ($PreRelease) { |
| 749 | Write-Host " Mode: Pre-release channel" -ForegroundColor Magenta |
| 750 | } |
| 751 | |
| 752 | Write-Host " Using $($vsceAvailability.CommandType)..." -ForegroundColor Gray |
| 753 | |
| 754 | # Execute vsce command using I/O function |
| 755 | $useWindowsWrapper = ($IsWindows -or $env:OS -eq 'Windows_NT') -and ($vsceCommand.Executable -eq 'npx') |
| 756 | $vsceResult = Invoke-VsceCommand ` |
| 757 | -Executable $vsceCommand.Executable ` |
| 758 | -Arguments $vsceCommand.Arguments ` |
| 759 | -WorkingDirectory $ExtensionDirectory ` |
| 760 | -UseWindowsWrapper:$useWindowsWrapper |
| 761 | |
| 762 | if (-not $vsceResult.Success) { |
| 763 | return New-PackagingResult -Success $false -ErrorMessage "vsce package command failed with exit code $($vsceResult.ExitCode)" |
| 764 | } |
| 765 | |
| 766 | # Find the generated vsix file |
| 767 | $vsixFile = Get-ChildItem -Path $ExtensionDirectory -Filter "*.vsix" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 |
| 768 | |
| 769 | if (-not $vsixFile) { |
| 770 | return New-PackagingResult -Success $false -ErrorMessage "No .vsix file found after packaging" |
| 771 | } |
| 772 | |
| 773 | Write-Host "" |
| 774 | Write-Host "โ
Extension packaged successfully!" -ForegroundColor Green |
| 775 | Write-Host " File: $($vsixFile.Name)" -ForegroundColor Cyan |
| 776 | Write-Host " Size: $([math]::Round($vsixFile.Length / 1KB, 2)) KB" -ForegroundColor Cyan |
| 777 | Write-Host " Version: $packageVersion" -ForegroundColor Cyan |
| 778 | |
| 779 | # Output for CI/CD consumption |
| 780 | Set-CIOutput -Name 'version' -Value $packageVersion |
| 781 | Set-CIOutput -Name 'vsix-file' -Value $vsixFile.Name |
| 782 | Set-CIOutput -Name 'pre-release' -Value $PreRelease.IsPresent |
| 783 | |
| 784 | Write-Host "" |
| 785 | Write-Host "๐ Done!" -ForegroundColor Green |
| 786 | Write-Host "" |
| 787 | |
| 788 | return New-PackagingResult -Success $true -OutputPath $vsixFile.FullName -Version $packageVersion |
| 789 | } |
| 790 | catch { |
| 791 | return New-PackagingResult -Success $false -ErrorMessage $_.Exception.Message |
| 792 | } |
| 793 | finally { |
| 794 | # Cleanup copied directories using I/O function |
| 795 | Write-Host "" |
| 796 | Write-Host "๐งน Cleaning up..." -ForegroundColor Yellow |
| 797 | Remove-PackagingArtifacts -ExtensionDirectory $ExtensionDirectory -DirectoryNames $dirsToClean |
| 798 | |
| 799 | # Restore original version if it was changed using I/O function |
| 800 | if ($versionWasModified) { |
| 801 | Write-Host "" |
| 802 | Write-Host "๐ Restoring original package.json version..." -ForegroundColor Yellow |
| 803 | Restore-PackageJsonVersion -PackageJsonPath $PackageJsonPath -PackageJson $packageJson -OriginalVersion $originalVersion |
| 804 | } |
| 805 | } |
| 806 | } |
| 807 | |
| 808 | #endregion Orchestration Functions |
| 809 | |
| 810 | #region Main Execution |
| 811 | try { |
| 812 | # Only execute main logic when run directly, not when dot-sourced |
| 813 | if ($MyInvocation.InvocationName -ne '.') { |
| 814 | $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path |
| 815 | $RepoRoot = (Get-Item "$ScriptDir/../..").FullName |
| 816 | $ExtensionDir = Join-Path $RepoRoot "extension" |
| 817 | |
| 818 | $result = Invoke-PackageExtension ` |
| 819 | -ExtensionDirectory $ExtensionDir ` |
| 820 | -RepoRoot $RepoRoot ` |
| 821 | -Version $Version ` |
| 822 | -DevPatchNumber $DevPatchNumber ` |
| 823 | -ChangelogPath $ChangelogPath ` |
| 824 | -PreRelease:$PreRelease |
| 825 | |
| 826 | if (-not $result.Success) { |
| 827 | Write-Error $result.ErrorMessage |
| 828 | exit 1 |
| 829 | } |
| 830 | exit 0 |
| 831 | } |
| 832 | } |
| 833 | catch { |
| 834 | Write-Error "Package Extension failed: $($_.Exception.Message)" |
| 835 | Write-CIAnnotation -Message $_.Exception.Message -Level Error |
| 836 | exit 1 |
| 837 | } |
| 838 | #endregion |
| 839 | |