microsoft/hve-core

Public

mirrored fromhttps://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/address-powershell-test-comments

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

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()]
60param(
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
76Import-Module (Join-Path $PSScriptRoot "../lib/Modules/CIHelpers.psm1") -Force
77
78#region Pure Functions
79
80function 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
116function 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
146function 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
191function 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
232function 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
271function 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
340function 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
395function 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
447function 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
503function 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
533function 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
581function 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
811try {
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}
833catch {
834 Write-Error "Package Extension failed: $($_.Exception.Message)"
835 Write-CIAnnotation -Message $_.Exception.Message -Level Error
836 exit 1
837}
838#endregion
839