diff --git a/.gitignore b/.gitignore index 0ce6b6347db..035570ff901 100644 --- a/.gitignore +++ b/.gitignore @@ -217,6 +217,7 @@ packages/*/etc/ # csharp emitter !packages/http-client-csharp/package-lock.json packages/http-client-csharp/generator/artifacts/ +packages/http-client-csharp/debug/ packages/http-client-csharp/generated-defs/**/*.js packages/http-client-csharp/generated-defs/**/*.js.map packages/http-client-csharp/generated-defs/**/*.d.ts diff --git a/cspell.yaml b/cspell.yaml index 8e8883a4737..0e318d142f6 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -6,6 +6,8 @@ dictionaries: - typescript words: - Adoptium + - Arize + - arizeaiobservabilityeval - agentic - aiohttp - alzimmer @@ -156,11 +158,13 @@ words: - myname - mypy - nanos + - ncpu - nexted - nihao - nint - NODEFS - noformat + - nologo - noopener - noreferrer - nosec diff --git a/packages/http-client-csharp/CONTRIBUTING.md b/packages/http-client-csharp/CONTRIBUTING.md index 53518283b9c..fad090866b7 100644 --- a/packages/http-client-csharp/CONTRIBUTING.md +++ b/packages/http-client-csharp/CONTRIBUTING.md @@ -176,11 +176,21 @@ Generate test projects to validate the emitter and generator: To regenerate test projects after making changes: -1. **Generate projects**: +**Generate projects**: - ```bash - ./eng/scripts/Generate.ps1 - ``` +```bash +./eng/scripts/Generate.ps1 +``` + +### Regenerating Azure Libraries + +To regenerate azure libraries using your local changes, run: + +```bash + ./eng/scripts/RegenPreview.ps1 +``` + +This will regenerate all the Azure libraries and allow you to view any potential diffs your changes may cause. For more information on the script's usage, see [RegenPreview](./eng/scripts/docs/RegenPreview.md). ## Creating Pull Requests diff --git a/packages/http-client-csharp/eng/scripts/RegenPreview.ps1 b/packages/http-client-csharp/eng/scripts/RegenPreview.ps1 new file mode 100644 index 00000000000..07600610248 --- /dev/null +++ b/packages/http-client-csharp/eng/scripts/RegenPreview.ps1 @@ -0,0 +1,1013 @@ +#Requires -Version 7.0 + +<# +.SYNOPSIS + Builds local generator packages and regenerates Azure SDK for .NET or OpenAI .NET libraries for validation. + +.DESCRIPTION + This script supports two modes: + + Azure SDK Mode (default): + 1. Builds a local npm package of @typespec/http-client-csharp with a versioned name (1.0.0-alpha.YYYYMMDD.hash) + 2. Builds and packages the three NuGet generator framework packages with the same versioning + 3. Updates Packages.Data.props in azure-sdk-for-net with the local NuGet version + 4. Updates the Azure generator (@azure-typespec/http-client-csharp) to use the local unbranded generator + 5. Builds and packages the Azure generator locally + 6. Updates the management plane generator (@azure-typespec/http-client-csharp-mgmt) to use local generators + 7. Updates the eng folder package.json artifacts in azure-sdk-for-net + 8. Regenerates libraries based on specified filters (all, by generator type, or interactively selected) + 9. Restores all modified artifacts to original state on success + + OpenAI Mode (when repository path contains "openai-dotnet"): + 1. Builds a local npm package of @typespec/http-client-csharp with a versioned name + 2. Builds and packages the NuGet generator framework packages + 3. Updates codegen/package.json with local unbranded generator + 4. Updates OpenAI.Library.Plugin.csproj with local NuGet package version + 5. Invokes Invoke-CodeGen.ps1 to regenerate the OpenAI library + 6. Restores all modified artifacts to original state on success + + Generator Filtering (Azure SDK Mode only): + - Use -Azure to regenerate only Azure-branded libraries (@azure-typespec/http-client-csharp) + - Use -Unbranded to regenerate only unbranded libraries (@typespec/http-client-csharp) + - Use -Mgmt to regenerate only management plane libraries (@azure-typespec/http-client-csharp-mgmt) + - Omit all filter parameters to regenerate all libraries (default) + - Use -Select for interactive selection (can be combined with generator filters) + +.PARAMETER SdkLibraryRepoPath + Required. The local file system path to the SDK repository (azure-sdk-for-net or openai-dotnet). + When the path contains "openai-dotnet", the script automatically operates in OpenAI mode. + +.PARAMETER Select + Optional. Azure SDK Mode only. When specified, displays an interactive menu to select specific libraries to regenerate. + If omitted, regenerates all libraries without prompting. + Not applicable in OpenAI mode. + +.PARAMETER Azure + Optional. Azure SDK Mode only. When specified, only regenerates libraries using the Azure generator (@azure-typespec/http-client-csharp). + Mutually exclusive with Unbranded and Mgmt parameters. + Not applicable in OpenAI mode. + +.PARAMETER Unbranded + Optional. Azure SDK Mode only. When specified, only regenerates libraries using the unbranded generator (@typespec/http-client-csharp). + Mutually exclusive with Azure and Mgmt parameters. + Not applicable in OpenAI mode. + +.PARAMETER Mgmt + Optional. Azure SDK Mode only. When specified, only regenerates libraries using the management plane generator (@azure-typespec/http-client-csharp-mgmt). + Mutually exclusive with Azure and Unbranded parameters. + If no generator filter is specified, all libraries are regenerated. + Not applicable in OpenAI mode. + +.EXAMPLE + # Azure SDK Mode - Regenerate all libraries + .\RegenPreview.ps1 -SdkLibraryRepoPath "C:\repos\azure-sdk-for-net" + +.EXAMPLE + # OpenAI Mode - Regenerate OpenAI library + .\RegenPreview.ps1 -SdkLibraryRepoPath "C:\repos\openai-dotnet" + +.EXAMPLE + # Azure SDK Mode - Interactively select libraries to regenerate (from all available) + .\RegenPreview.ps1 -SdkLibraryRepoPath "C:\repos\azure-sdk-for-net" -Select + +.EXAMPLE + # Regenerate only Azure-branded libraries (non-interactive) + .\RegenPreview.ps1 -SdkLibraryRepoPath "C:\repos\azure-sdk-for-net" -Azure + +.EXAMPLE + # Interactively select from Azure-branded libraries only + .\RegenPreview.ps1 -SdkLibraryRepoPath "C:\repos\azure-sdk-for-net" -Azure -Select + +.EXAMPLE + # Regenerate only unbranded libraries + .\RegenPreview.ps1 -SdkLibraryRepoPath "C:\repos\azure-sdk-for-net" -Unbranded + +.EXAMPLE + # Regenerate only management plane libraries + .\RegenPreview.ps1 -SdkLibraryRepoPath "C:\repos\azure-sdk-for-net" -Mgmt +#> + +param( + [Parameter(Mandatory=$true)] + [string]$SdkLibraryRepoPath, + + [Parameter(Mandatory=$false)] + [switch]$Select, + + [Parameter(Mandatory=$false)] + [switch]$Azure, + + [Parameter(Mandatory=$false)] + [switch]$Unbranded, + + [Parameter(Mandatory=$false)] + [switch]$Mgmt +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version 3.0 + +# Detect OpenAI mode +$isOpenAIMode = $SdkLibraryRepoPath -like "*openai-dotnet*" + +# Validate mutually exclusive parameters +if ($isOpenAIMode) { + # In OpenAI mode, no filter parameters are allowed + if ($Select -or $Azure -or $Unbranded -or $Mgmt) { + Write-Error "OpenAI mode detected. The -Select, -Azure, -Unbranded, and -Mgmt parameters are not applicable when regenerating OpenAI libraries." + exit 1 + } +} else { + # In Azure SDK mode, validate filter parameters + $generatorFilters = @($Azure, $Unbranded, $Mgmt) + $activeFilters = @($generatorFilters | Where-Object { $_ }).Count + + if ($activeFilters -gt 1) { + Write-Error "Parameters -Azure, -Unbranded, and -Mgmt are mutually exclusive. Please specify only one." + exit 1 + } +} + +# Import utility functions +Import-Module "$PSScriptRoot\Generation.psm1" -DisableNameChecking -Force +Import-Module "$PSScriptRoot\RegenPreview.psm1" -DisableNameChecking -Force + +# Resolve paths +$packageRoot = Resolve-Path (Join-Path $PSScriptRoot '..' '..') +$sdkRepoPath = Resolve-Path $SdkLibraryRepoPath -ErrorAction Stop + +Write-Host "==================== LOCAL VALIDATION SCRIPT ====================" -ForegroundColor Cyan + +if ($isOpenAIMode) { + Write-Host "Repository: OpenAI .NET" -ForegroundColor Gray + Write-Host "Mode: OpenAI library regeneration" -ForegroundColor Yellow +} else { + Write-Host "Repository: Azure SDK for .NET" -ForegroundColor Gray + + # Display active mode + $modeText = if ($Select) { + "Interactive library selection" + } elseif ($Azure) { + "Regenerate Azure SDK libraries only" + } elseif ($Unbranded) { + "Regenerate Unbranded libraries only" + } elseif ($Mgmt) { + "Regenerate Management plane libraries only" + } else { + "Regenerate ALL libraries" + } + Write-Host "Mode: $modeText" -ForegroundColor Yellow +} +Write-Host "" + +# Generate version string with timestamp and hash +# Used for both npm and NuGet packages to ensure consistency +function Get-LocalPackageVersion { + $timestamp = Get-Date -Format "yyyyMMdd" + $hash = (git -C $packageRoot rev-parse --short HEAD 2>$null) ?? "local" + return "1.0.0-alpha.$timestamp.$hash" +} + +# Run npm pack and return the package file path +function Invoke-NpmPack { + param( + [string]$WorkingDirectory, + [string]$DebugFolder + ) + + Write-Host "Running: npm pack" -ForegroundColor Gray + Push-Location $WorkingDirectory + try { + $output = & npm pack 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "npm pack failed with exit code $LASTEXITCODE" + } + + # Get the first line that ends with .tgz (the actual package filename) + # It may be in format "filename: package-name.tgz" or just "package-name.tgz" + $packageLine = ($output | Where-Object { $_ -match '\.tgz$' } | Select-Object -First 1).ToString().Trim() + if ($packageLine -match 'filename:\s*(.+\.tgz)') { + $packageFile = $Matches[1].Trim() + } else { + $packageFile = $packageLine + } + + $packagePath = Join-Path $WorkingDirectory $packageFile + if (-not (Test-Path $packagePath)) { + throw "Package file not created: $packagePath" + } + + # Move package to debug folder + $debugPackagePath = Join-Path $DebugFolder $packageFile + Move-Item $packagePath $debugPackagePath -Force + + return $debugPackagePath + } + finally { + Pop-Location + } +} + +# Update package.json with new version +function Update-PackageJsonVersion { + param( + [string]$PackageJsonPath, + [string]$NewVersion + ) + + Write-Host "Updating package version to $NewVersion in $PackageJsonPath" -ForegroundColor Gray + $packageJson = Get-Content $PackageJsonPath -Raw | ConvertFrom-Json -AsHashtable + $packageJson.version = $NewVersion + $packageJson | ConvertTo-Json -Depth 100 | Set-Content $PackageJsonPath -Encoding utf8 -NoNewline +} + +# Update UnbrandedGeneratorVersion in Packages.Data.props +function Update-UnbrandedGeneratorVersion { + param( + [string]$PackagesDataPropsPath, + [string]$NewVersion + ) + + Write-Host "Updating UnbrandedGeneratorVersion to $NewVersion in Packages.Data.props" -ForegroundColor Gray + + $content = Get-Content $PackagesDataPropsPath -Raw + + # Use regex to find and replace the UnbrandedGeneratorVersion property + $pattern = '()([^<]+)()' + + if ($content -match $pattern) { + $oldVersion = $Matches[2] + $newContent = $content -replace $pattern, "$NewVersion" + Set-Content $PackagesDataPropsPath -Value $newContent -Encoding utf8 -NoNewline + Write-Host " Updated UnbrandedGeneratorVersion from $oldVersion to $NewVersion" -ForegroundColor Green + } else { + throw "UnbrandedGeneratorVersion property not found in $PackagesDataPropsPath" + } +} + +# Parse Library_Inventory.md to get libraries +function Get-LibrariesToRegenerate { + param([string]$InventoryPath) + + $libraries = @() + $content = Get-Content $InventoryPath -Raw + + # Helper function to parse library section + $parseSection = { + param($SectionContent, $GeneratorName) + + $lines = $SectionContent -split "`n" | Where-Object { + $_ -match '^\|.*\|.*\|.*\|' -and + $_ -notmatch '^\|\s*Service\s*\|' -and + $_ -notmatch '^\|\s*-+\s*\|' -and + $_.Trim() -ne '' + } + + $result = @() + foreach ($line in $lines) { + $parts = $line -split '\|' | ForEach-Object { $_.Trim() } | Where-Object { $_ -and $_ -notmatch '^-+$' } + if ($parts.Count -eq 3 -and $parts[0] -ne 'Service' -and $parts[0] -notmatch '^-+$') { + $result += @{ + Service = $parts[0] + Library = $parts[1] + Path = $parts[2] + Generator = $GeneratorName + } + } + } + return $result + } + + # Parse @azure-typespec/http-client-csharp libraries + if ($content -match '## Data Plane Libraries using TypeSpec \(@azure-typespec/http-client-csharp\)[\s\S]*?Total: (\d+)([\s\S]*?)(?=##|\z)') { + $libraries += & $parseSection $Matches[2] "@azure-typespec/http-client-csharp" + } + + # Parse @typespec/http-client-csharp libraries + if ($content -match '## Data Plane Libraries using TypeSpec \(@typespec/http-client-csharp\)[\s\S]*?Total: (\d+)([\s\S]*?)(?=##|\z)') { + $libraries += & $parseSection $Matches[2] "@typespec/http-client-csharp" + } + + # Parse @azure-typespec/http-client-csharp-mgmt libraries (management plane) + if ($content -match '## Management Plane Libraries using TypeSpec \(@azure-typespec/http-client-csharp-mgmt\)[\s\S]*?Total: (\d+)([\s\S]*?)(?=##|\z)') { + $libraries += & $parseSection $Matches[2] "@azure-typespec/http-client-csharp-mgmt" + } + + return @($libraries) +} + +# Interactive library selection +function Select-LibrariesToRegenerate { + param([array]$Libraries) + + # Ensure we have an array + if (-not $Libraries) { + return @() + } + + $Libraries = @($Libraries) + + Write-Host "`n==================== LIBRARY SELECTION ====================" -ForegroundColor Cyan + Write-Host "Found $($Libraries.Count) libraries available for regeneration" -ForegroundColor White + Write-Host "" + + # Display libraries grouped by generator + $azureLibs = @($Libraries | Where-Object { $_.Generator -eq "@azure-typespec/http-client-csharp" }) + $unbrandedLibs = @($Libraries | Where-Object { $_.Generator -eq "@typespec/http-client-csharp" }) + $mgmtLibs = @($Libraries | Where-Object { $_.Generator -eq "@azure-typespec/http-client-csharp-mgmt" }) + + $currentIndex = 1 + + if ($azureLibs.Count -gt 0) { + Write-Host "Azure SDK Libraries:" -ForegroundColor Yellow + for ($i = 0; $i -lt $azureLibs.Count; $i++) { + $lib = $azureLibs[$i] + Write-Host (" [{0,2}] {1,-50} ({2})" -f $currentIndex, $lib.Library, $lib.Service) -ForegroundColor Gray + $currentIndex++ + } + Write-Host "" + } + + if ($unbrandedLibs.Count -gt 0) { + Write-Host "Unbranded Libraries:" -ForegroundColor Yellow + for ($i = 0; $i -lt $unbrandedLibs.Count; $i++) { + $lib = $unbrandedLibs[$i] + Write-Host (" [{0,2}] {1,-50} ({2})" -f $currentIndex, $lib.Library, $lib.Service) -ForegroundColor Gray + $currentIndex++ + } + Write-Host "" + } + + if ($mgmtLibs.Count -gt 0) { + Write-Host "Management Plane Libraries:" -ForegroundColor Yellow + for ($i = 0; $i -lt $mgmtLibs.Count; $i++) { + $lib = $mgmtLibs[$i] + Write-Host (" [{0,2}] {1,-50} ({2})" -f $currentIndex, $lib.Library, $lib.Service) -ForegroundColor Gray + $currentIndex++ + } + Write-Host "" + } + + Write-Host "=============================================================" -ForegroundColor Cyan + Write-Host "" + Write-Host "Enter library numbers to regenerate (comma-separated), 'all' for all libraries, or 'q' to quit:" -ForegroundColor White + Write-Host "Example: 1,3,5 or 1-4,7 or all" -ForegroundColor DarkGray + Write-Host "" + + $selection = Read-Host "Selection" + + if ($selection -ieq 'q' -or $selection -ieq 'quit') { + Write-Host "Operation cancelled by user." -ForegroundColor Yellow + exit 0 + } + + if ($selection -ieq 'all') { + return $Libraries + } + + # Parse selection + $selectedIndices = @() + $parts = $selection -split ',' | ForEach-Object { $_.Trim() } + + foreach ($part in $parts) { + if ($part -match '^(\d+)-(\d+)$') { + # Range: 1-4 + $start = [int]$Matches[1] + $end = [int]$Matches[2] + $selectedIndices += ($start..$end) + } + elseif ($part -match '^\d+$') { + # Single number: 3 + $selectedIndices += [int]$part + } + else { + Write-Host "Invalid selection format: $part" -ForegroundColor Red + exit 1 + } + } + + # Validate and collect selected libraries + $selectedLibraries = @() + foreach ($index in $selectedIndices | Sort-Object -Unique) { + if ($index -lt 1 -or $index -gt $Libraries.Count) { + Write-Host "Invalid library number: $index (valid range: 1-$($Libraries.Count))" -ForegroundColor Red + exit 1 + } + $selectedLibraries += $Libraries[$index - 1] + } + + if ($selectedLibraries.Count -eq 0) { + Write-Host "No libraries selected. Exiting." -ForegroundColor Yellow + exit 0 + } + + Write-Host "`nSelected $($selectedLibraries.Count) libraries for regeneration:" -ForegroundColor Green + foreach ($lib in $selectedLibraries) { + Write-Host " - $($lib.Library) ($($lib.Service))" -ForegroundColor Gray + } + Write-Host "" + + # Ensure we return an array, even for a single element + return @($selectedLibraries) +} + +# Generate final report +function Write-RegenerationReport { + param( + [array]$Results, + [TimeSpan]$ElapsedTime, + [string]$DebugFolder + ) + + $passed = @($Results | Where-Object { $_.Success -eq $true }) + $failed = @($Results | Where-Object { $_.Success -eq $false }) + + Write-Host "`n==================== REGENERATION REPORT ====================" -ForegroundColor Cyan + Write-Host "Total Libraries: $($Results.Count)" -ForegroundColor White + Write-Host "Passed: $($passed.Count)" -ForegroundColor Green + Write-Host "Failed: $($failed.Count)" -ForegroundColor Red + + if ($ElapsedTime) { + $elapsedFormatted = "{0:hh\:mm\:ss}" -f $ElapsedTime + Write-Host "Execution Time: $elapsedFormatted" -ForegroundColor Cyan + } + Write-Host "" + + if ($passed.Count -gt 0) { + Write-Host "PASSED LIBRARIES:" -ForegroundColor Green + foreach ($result in $passed) { + Write-Host " ✓ $($result.Library) ($($result.Service))" -ForegroundColor Green + } + Write-Host "" + } + + if ($failed.Count -gt 0) { + Write-Host "FAILED LIBRARIES:" -ForegroundColor Red + foreach ($result in $failed) { + Write-Host " ✗ $($result.Library) ($($result.Service))" -ForegroundColor Red + Write-Host " Error: $($result.Error)" -ForegroundColor Gray + if ($result.Output) { + Write-Host " Details: $($result.Output.Substring(0, [Math]::Min(200, $result.Output.Length)))..." -ForegroundColor DarkGray + } + } + Write-Host "" + } + + Write-Host "=============================================================" -ForegroundColor Cyan + + # Save detailed report to debug folder + $reportPath = if ($DebugFolder) { + Join-Path $DebugFolder "regen-report.json" + } else { + Join-Path $packageRoot "regen-report.json" + } + $Results | ConvertTo-Json -Depth 10 | Set-Content $reportPath -Encoding utf8 + Write-Host "Detailed report saved to: $reportPath" -ForegroundColor Gray +} + +# ============================================================================ +# Main Script Execution +# ============================================================================ + +# Start timer +$scriptStartTime = Get-Date + +try { + # Step 1: Load and select libraries (Azure SDK Mode only, if using -Select flag) + if ($Select -and -not $isOpenAIMode) { + Write-Host "`n[1/5] Loading libraries from Library_Inventory.md..." -ForegroundColor Cyan + + $inventoryPath = Join-Path $sdkRepoPath "doc" "GeneratorMigration" "Library_Inventory.md" + if (-not (Test-Path $inventoryPath)) { + throw "Library_Inventory.md not found at: $inventoryPath" + } + + $allLibraries = Get-LibrariesToRegenerate -InventoryPath $inventoryPath + + # Apply generator filter before interactive selection + $filteredLibraries = @(Filter-LibrariesByGenerator ` + -Libraries $allLibraries ` + -Azure:$Azure ` + -Unbranded:$Unbranded ` + -Mgmt:$Mgmt) + + if (-not $filteredLibraries -or $filteredLibraries.Count -eq 0) { + Write-Host "No libraries found matching the specified generator filter" -ForegroundColor Yellow + exit 0 + } + + $libraries = @(Select-LibrariesToRegenerate -Libraries $filteredLibraries) + + # Check if user cancelled selection + if (-not $libraries -or $libraries.Count -eq 0) { + Write-Host "No libraries selected. Exiting..." -ForegroundColor Yellow + exit 0 + } + } + + # Create debug folder for packaged artifacts with timestamped subfolder + $timestamp = Get-Date -Format "yyyyMMdd" + $debugFolder = Join-Path $packageRoot "debug" $timestamp + if (-not (Test-Path $debugFolder)) { + New-Item -ItemType Directory -Path $debugFolder -Force | Out-Null + } + + Write-Host "Debug folder: $debugFolder" -ForegroundColor Gray + Write-Host "" + + # Step 1: Build the unbranded generator + if ($isOpenAIMode) { + Write-Host "`n[1/3] Building unbranded generator..." -ForegroundColor Cyan + } else { + Write-Host "`n[2/5] Building unbranded generator..." -ForegroundColor Cyan + } + + Push-Location $packageRoot + try { + Write-Host "Installing dependencies..." -ForegroundColor Gray + Invoke "npm ci" + if ($LASTEXITCODE -ne 0) { + throw "Failed to install dependencies for unbranded generator" + } + + Write-Host "Cleaning build artifacts..." -ForegroundColor Gray + Invoke "npm run clean" + if ($LASTEXITCODE -ne 0) { + throw "Failed to clean unbranded generator" + } + + Write-Host "Building generator..." -ForegroundColor Gray + Invoke "npm run build" + if ($LASTEXITCODE -ne 0) { + throw "Failed to build unbranded generator" + } + + Write-Host " Build completed" -ForegroundColor Green + } + finally { + Pop-Location + } + + # Step 2: Package the generators with local version + if ($isOpenAIMode) { + Write-Host "`n[2/3] Packaging generators..." -ForegroundColor Cyan + } else { + Write-Host "`n[2/5] Packaging generators..." -ForegroundColor Cyan + } + + $localVersion = Get-LocalPackageVersion + Write-Host "Local package version: $localVersion" -ForegroundColor Yellow + + $unbrandedPackageJson = Join-Path $packageRoot "package.json" + $originalPackageJson = Get-Content $unbrandedPackageJson -Raw + + try { + Update-PackageJsonVersion -PackageJsonPath $unbrandedPackageJson -NewVersion $localVersion + $unbrandedPackagePath = Invoke-NpmPack -WorkingDirectory $packageRoot -DebugFolder $debugFolder + Write-Host "Created npm package: $unbrandedPackagePath" -ForegroundColor Green + } + finally { + # Restore original package.json + Set-Content $unbrandedPackageJson $originalPackageJson -Encoding utf8 -NoNewline + } + + # Build and package NuGet packages for generator framework + Write-Host "Building NuGet generator packages..." -ForegroundColor Gray + + $generatorRoot = Join-Path $packageRoot "generator" + $nugetProjects = @( + "Microsoft.TypeSpec.Generator\src\Microsoft.TypeSpec.Generator.csproj", + "Microsoft.TypeSpec.Generator.Input\src\Microsoft.TypeSpec.Generator.Input.csproj", + "Microsoft.TypeSpec.Generator.ClientModel\src\Microsoft.TypeSpec.Generator.ClientModel.csproj" + ) + + foreach ($project in $nugetProjects) { + $projectPath = Join-Path $generatorRoot $project + if (-not (Test-Path $projectPath)) { + throw "NuGet project not found: $projectPath" + } + + Write-Host "Packing: $(Split-Path $projectPath -Leaf)" -ForegroundColor Gray + $packCmd = "dotnet pack $projectPath /p:Version=$localVersion /p:PackageVersion=$localVersion /p:PackageOutputPath=$debugFolder --configuration Debug --no-build --nologo -v:quiet" + Invoke $packCmd $generatorRoot + } + + Write-Host " NuGet packages created" -ForegroundColor Green + + # OpenAI Mode: Update and regenerate OpenAI library + if ($isOpenAIMode) { + Write-Host "`n[3/3] Updating and regenerating OpenAI library..." -ForegroundColor Cyan + + try { + $generationOutput = Update-OpenAIGenerator ` + -OpenAIRepoPath $sdkRepoPath ` + -UnbrandedPackagePath $unbrandedPackagePath ` + -LocalVersion $localVersion ` + -DebugFolder $debugFolder + + # Create result object for successful regeneration + $result = @{ + Success = $true + Library = "OpenAI .NET Library" + Service = "OpenAI" + Path = "codegen" + Generator = "@typespec/http-client-csharp" + Error = "" + Output = $generationOutput + } + } + catch { + # Create result object for failed regeneration + $result = @{ + Success = $false + Library = "OpenAI .NET Library" + Service = "OpenAI" + Path = "codegen" + Generator = "@typespec/http-client-csharp" + Error = $_.Exception.Message + Output = $_.Exception.ToString() + } + } + + # Calculate elapsed time and generate report + $scriptEndTime = Get-Date + $elapsedTime = $scriptEndTime - $scriptStartTime + + Write-RegenerationReport -Results @($result) -ElapsedTime $elapsedTime -DebugFolder $debugFolder + + # Exit with appropriate code + if ($result.Success) { + Write-Host "`nScript completed successfully." -ForegroundColor Cyan + exit 0 + } else { + Write-Host "`nScript failed." -ForegroundColor Red + exit 1 + } + } + + # Azure SDK Mode: Continue with Azure SDK-specific steps + + # Update Packages.Data.props with local NuGet version + $packagesDataPropsPath = Join-Path $sdkRepoPath "eng" "Packages.Data.props" + if (-not (Test-Path $packagesDataPropsPath)) { + throw "Packages.Data.props not found at: $packagesDataPropsPath" + } + + Update-UnbrandedGeneratorVersion -PackagesDataPropsPath $packagesDataPropsPath -NewVersion $localVersion + + # Add debug folder as a NuGet package source + $nugetConfigPath = Join-Path $sdkRepoPath "NuGet.Config" + Add-LocalNuGetSource -NuGetConfigPath $nugetConfigPath -SourcePath $debugFolder + + # Step 3: Build required generators and update artifacts + Write-Host "`n[3/5] Building required generators and updating artifacts..." -ForegroundColor Cyan + + # Determine which generators are needed based on libraries to be regenerated + if ($Select) { + # Libraries were already selected at the beginning + $librariesToAnalyze = $libraries + } else { + # Load all libraries and apply filters to determine what would be regenerated + $inventoryPath = Join-Path $sdkRepoPath "doc" "GeneratorMigration" "Library_Inventory.md" + if (-not (Test-Path $inventoryPath)) { + throw "Library_Inventory.md not found at: $inventoryPath" + } + + $allLibraries = Get-LibrariesToRegenerate -InventoryPath $inventoryPath + $librariesToAnalyze = Filter-LibrariesByGenerator ` + -Libraries $allLibraries ` + -Azure:$Azure ` + -Unbranded:$Unbranded ` + -Mgmt:$Mgmt + } + + # Determine which generators are actually needed + $needsUnbranded = $false + $needsAzure = $false + $needsMgmt = $false + + if ($librariesToAnalyze -and $librariesToAnalyze.Count -gt 0) { + foreach ($lib in $librariesToAnalyze) { + switch ($lib.Generator) { + "@typespec/http-client-csharp" { $needsUnbranded = $true } + "@azure-typespec/http-client-csharp" { $needsAzure = $true; $needsUnbranded = $true } + "@azure-typespec/http-client-csharp-mgmt" { $needsMgmt = $true; $needsAzure = $true; $needsUnbranded = $true } + } + } + } + + $generatorSummary = @() + if ($needsUnbranded) { $generatorSummary += "Unbranded" } + if ($needsAzure) { $generatorSummary += "Azure" } + if ($needsMgmt) { $generatorSummary += "Management" } + + if ($generatorSummary.Count -gt 0) { + Write-Host "Required generators: $($generatorSummary -join ', ')" -ForegroundColor Yellow + } else { + Write-Host "No additional generators required" -ForegroundColor Yellow + } + + # Update Azure generator if needed + if ($needsAzure) { + Write-Host "Building Azure generator..." -ForegroundColor Gray + + $azureGeneratorPath = Join-Path $sdkRepoPath "eng" "packages" "http-client-csharp" + $packagesDataPropsPath = Join-Path $sdkRepoPath "eng" "Packages.Data.props" + + $azurePackagePath = Update-AzureGenerator ` + -AzureGeneratorPath $azureGeneratorPath ` + -UnbrandedPackagePath $unbrandedPackagePath ` + -DebugFolder $debugFolder ` + -PackagesDataPropsPath $packagesDataPropsPath ` + -LocalVersion $localVersion + + Write-Host " Azure generator completed" -ForegroundColor Green + } else { + $azurePackagePath = $null + } + + # Update eng folder artifacts if needed + if ($needsAzure -or $needsUnbranded) { + Write-Host "Updating eng folder artifacts..." -ForegroundColor Gray + + $engFolder = Join-Path $sdkRepoPath "eng" + $tempDir = Join-Path $engFolder "temp-package-update" + New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + + # Helper function to update emitter package files + $updateEmitterPackage = { + param($PackagePath, $EmitterJsonName, $LockJsonName) + + $emitterJson = Join-Path $engFolder $EmitterJsonName + $tempPackageJson = Join-Path $tempDir "package.json" + + Copy-Item $emitterJson $tempPackageJson -Force + + Push-Location $tempDir + try { + Invoke "npm install `"`"file:$PackagePath`"`" --package-lock-only" $tempDir + if ($LASTEXITCODE -ne 0) { + throw "Failed to install local package for emitter update." + } + + Copy-Item $tempPackageJson $emitterJson -Force + $lockFile = Join-Path $tempDir "package-lock.json" + if (Test-Path $lockFile) { + Copy-Item $lockFile (Join-Path $engFolder $LockJsonName) -Force + } + + # Cleanup temp files + Remove-Item $tempPackageJson, $lockFile -Force -ErrorAction SilentlyContinue + } + finally { + Pop-Location + } + } + + try { + if ($needsAzure -and $azurePackagePath) { + & $updateEmitterPackage $azurePackagePath "azure-typespec-http-client-csharp-emitter-package.json" "azure-typespec-http-client-csharp-emitter-package-lock.json" + } + + if ($needsUnbranded) { + & $updateEmitterPackage $unbrandedPackagePath "http-client-csharp-emitter-package.json" "http-client-csharp-emitter-package-lock.json" + } + + Write-Host " Artifacts updated" -ForegroundColor Green + } + finally { + Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + } + + # Update management plane generator if needed + if ($needsMgmt) { + Write-Host "Building management plane generator..." -ForegroundColor Gray + + $engFolder = Join-Path $sdkRepoPath "eng" + $mgmtGeneratorPath = Join-Path $engFolder "packages" "http-client-csharp-mgmt" + + if (Test-Path $mgmtGeneratorPath) { + Update-MgmtGenerator ` + -EngFolder $engFolder ` + -DebugFolder $debugFolder ` + -LocalVersion $localVersion + + Write-Host " Management plane generator completed" -ForegroundColor Green + } else { + Write-Host " Management plane generator not found, skipping..." -ForegroundColor Yellow + } + } + + # Step 4: Regenerate libraries + Write-Host "`n[4/5] Regenerating libraries..." -ForegroundColor Cyan + + if (-not $Select) { + # Load all libraries if not using -Select flag + $inventoryPath = Join-Path $sdkRepoPath "doc" "GeneratorMigration" "Library_Inventory.md" + if (-not (Test-Path $inventoryPath)) { + throw "Library_Inventory.md not found at: $inventoryPath" + } + + $allLibraries = Get-LibrariesToRegenerate -InventoryPath $inventoryPath + + # Apply generator filter + $libraries = Filter-LibrariesByGenerator ` + -Libraries $allLibraries ` + -Azure:$Azure ` + -Unbranded:$Unbranded ` + -Mgmt:$Mgmt + + if ($libraries.Count -eq 0) { + Write-Host "No libraries found matching the specified generator filter" -ForegroundColor Yellow + Write-Host "Skipping regeneration step..." -ForegroundColor Gray + } else { + $filterText = if ($Azure) { + " (Azure generator only)" + } elseif ($Unbranded) { + " (Unbranded generator only)" + } elseif ($Mgmt) { + " (Management plane generator only)" + } else { + "" + } + Write-Host "Regenerating $($libraries.Count) libraries$filterText" -ForegroundColor Yellow + } + } else { + # Libraries were already selected at the beginning + if ($libraries -and $libraries.Count -gt 0) { + Write-Host "Using $($libraries.Count) previously selected libraries" -ForegroundColor Yellow + } else { + Write-Host "No libraries were selected" -ForegroundColor Yellow + } + } + + if (-not $libraries -or $libraries.Count -eq 0) { + Write-Host "No libraries selected for regeneration" -ForegroundColor Yellow + $failedCount = 0 + } else { + + # Determine parallel execution throttle limit: (CPU cores - 2), min 1, max 8 + $cpuCores = if ($IsWindows -or $PSVersionTable.PSVersion.Major -lt 6) { + (Get-CimInstance -ClassName Win32_Processor | Measure-Object -Property NumberOfLogicalProcessors -Sum).Sum + } elseif ($IsMacOS) { + [int](sysctl -n hw.ncpu) + } else { + [int](nproc) + } + + $throttleLimit = [Math]::Max(1, [Math]::Min(8, $cpuCores - 2)) + + Write-Host "Using $throttleLimit concurrent jobs (detected $cpuCores logical processors)" -ForegroundColor Gray + Write-Host "" + + # Pre-install tsp-client to avoid concurrent npm operations + Write-Host "Pre-installing tsp-client..." -ForegroundColor Gray + $tspClientDir = Join-Path $sdkRepoPath "eng" "common" "tsp-client" + Invoke "npm ci --prefix $tspClientDir" $tspClientDir + if ($LASTEXITCODE -ne 0) { + throw "Failed to install tsp-client" + } + Write-Host " tsp-client ready" -ForegroundColor Green + Write-Host "" + + # Thread-safe collections for progress tracking + $completed = [System.Collections.Concurrent.ConcurrentBag[int]]::new() + $totalCount = $libraries.Count + + # Run regeneration in parallel + $results = $libraries | ForEach-Object -ThrottleLimit $throttleLimit -Parallel { + $library = $_ + $azureSdkPath = $using:sdkRepoPath + $completedBag = $using:completed + $total = $using:totalCount + + # Determine build path (check for src subdirectory) + $libraryPath = Join-Path $azureSdkPath $library.Path + $srcPath = Join-Path $libraryPath "src" + $buildPath = if ((Test-Path $srcPath) -and (Get-ChildItem -Path $srcPath -Filter "*.csproj" -ErrorAction SilentlyContinue)) { + $srcPath + } else { + $libraryPath + } + + # Regenerate library + $result = try { + if (-not (Test-Path $libraryPath)) { + @{ Success = $false; Error = "Library path not found"; Output = "" } + } else { + Push-Location $buildPath + try { + $output = & dotnet build /t:GenerateCode /p:SkipTspClientInstall=true 2>&1 + $exitCode = $LASTEXITCODE + + if ($exitCode -ne 0) { + @{ Success = $false; Error = "Generation failed with exit code $exitCode"; Output = ($output -join "`n") } + } else { + @{ Success = $true; Output = ($output -join "`n") } + } + } + finally { + Pop-Location + } + } + } + catch { + @{ Success = $false; Error = $_.Exception.Message; Output = $_.Exception.ToString() } + } + + # Update progress counter + $completedBag.Add(1) + $currentCount = $completedBag.Count + + # Thread-safe console output with progress + $status = if ($result.Success) { "✓" } else { "✗" } + $color = if ($result.Success) { "Green" } else { "White" } + + $progressMsg = "[$currentCount/$total] $status $($library.Library)" + Write-Host $progressMsg -ForegroundColor $color + + # Return result with library metadata + return @{ + Service = $library.Service + Library = $library.Library + Path = $library.Path + Generator = $library.Generator + Success = if ($result.ContainsKey('Success')) { $result.Success } else { $false } + Error = if ($result.ContainsKey('Error')) { $result.Error } else { "" } + Output = if ($result.ContainsKey('Output')) { $result.Output } else { "" } + } + } + + # Generate final report + $scriptEndTime = Get-Date + $elapsedTime = $scriptEndTime - $scriptStartTime + + Write-RegenerationReport -Results $results -ElapsedTime $elapsedTime -DebugFolder $debugFolder + + # Check if any libraries failed + $failedLibraries = @($results | Where-Object { -not $_.Success }) + $failedCount = $failedLibraries.Count + } + + if ($failedCount -gt 0) { + Write-Host "`nValidation completed with warnings: $failedCount libraries failed to regenerate" -ForegroundColor Yellow + Write-Host "Check the detailed report above for error information" -ForegroundColor Yellow + } else { + Write-Host "`nValidation completed successfully! All libraries regenerated without errors." -ForegroundColor Green + + # Step 5: Restore artifacts to original state + Write-Host "`n[5/5] Restoring artifacts to original state..." -ForegroundColor Cyan + + Push-Location $sdkRepoPath + try { + Write-Host "Restoring modified files..." -ForegroundColor Gray + $filesToRestore = @( + "eng/azure-typespec-http-client-csharp-emitter-package.json" + "eng/azure-typespec-http-client-csharp-emitter-package-lock.json" + "eng/http-client-csharp-emitter-package.json" + "eng/http-client-csharp-emitter-package-lock.json" + "eng/azure-typespec-http-client-csharp-mgmt-emitter-package.json" + "eng/azure-typespec-http-client-csharp-mgmt-emitter-package-lock.json" + "eng/packages/http-client-csharp/package-lock.json" + "eng/packages/http-client-csharp-mgmt/package.json" + "eng/packages/http-client-csharp-mgmt/package-lock.json" + "eng/Packages.Data.props" + "NuGet.Config" + ) + $restoreCmd = "git restore $($filesToRestore -join ' ')" + Invoke $restoreCmd $sdkRepoPath + if ($LASTEXITCODE -ne 0) { + throw "Failed to restore modified files" + } + Write-Host " All artifacts restored" -ForegroundColor Green + } + finally { + Pop-Location + } + } +} +catch { + Write-Host "`nScript encountered an error during setup or configuration." -ForegroundColor Red + Write-Host $_.Exception.Message -ForegroundColor Red + Write-Host $_.ScriptStackTrace -ForegroundColor DarkGray + + # Attempt to restore NuGet.Config even on failure + Write-Host "`nAttempting to restore NuGet.Config..." -ForegroundColor Yellow + Push-Location $sdkRepoPath -ErrorAction SilentlyContinue + try { + #& git restore NuGet.Config 2>&1 | Out-Null + Invoke "git restore NuGet.Config" $sdkRepoPath + Write-Host " NuGet.Config restored" -ForegroundColor Green + } + catch { + Write-Host " Failed to restore NuGet.Config" -ForegroundColor Red + } + finally { + Pop-Location -ErrorAction SilentlyContinue + } + + exit 1 +} + +Write-Host "`nScript completed." -ForegroundColor Cyan diff --git a/packages/http-client-csharp/eng/scripts/RegenPreview.psm1 b/packages/http-client-csharp/eng/scripts/RegenPreview.psm1 new file mode 100644 index 00000000000..9d1220faa5f --- /dev/null +++ b/packages/http-client-csharp/eng/scripts/RegenPreview.psm1 @@ -0,0 +1,775 @@ +Import-Module "$PSScriptRoot\Generation.psm1" -DisableNameChecking -Force -Global + +function Update-GeneratorPackage { + <# + .SYNOPSIS + Common helper function to update, build, and package a TypeSpec generator. + + .DESCRIPTION + This internal function handles the common workflow for updating TypeSpec generators: + 1. Updates package.json dependencies + 2. Runs npm install + 3. Runs npm clean to ensure a clean build + 4. Builds the generator + 5. Packages the generator with specified version + 6. Moves package to debug folder + + This is a shared helper used by Update-MgmtGenerator and Update-AzureGenerator. + + .PARAMETER GeneratorPath + Path to the generator directory. + + .PARAMETER Dependencies + Hashtable of dependencies to update (name -> file path). + + .PARAMETER DevDependencies + Hashtable of dev dependencies to update (name -> file path). + + .PARAMETER LocalVersion + The version string to use for the package. + + .PARAMETER DebugFolder + The debug folder where the package should be moved. + + .PARAMETER UseNpmCi + If true, runs 'npm install --package-lock-only && npm ci' instead of 'npm install'. + #> + param( + [Parameter(Mandatory=$true)] + [string]$GeneratorPath, + + [Parameter(Mandatory=$false)] + [hashtable]$Dependencies = @{}, + + [Parameter(Mandatory=$false)] + [hashtable]$DevDependencies = @{}, + + [Parameter(Mandatory=$true)] + [string]$LocalVersion, + + [Parameter(Mandatory=$true)] + [string]$DebugFolder, + + [Parameter(Mandatory=$false)] + [bool]$UseNpmCi = $false + ) + + $ErrorActionPreference = 'Stop' + + $packageJsonPath = Join-Path $GeneratorPath "package.json" + $originalPackageJson = Get-Content $packageJsonPath -Raw + + try { + # Step 1: Update package.json dependencies + if ($Dependencies.Count -gt 0 -or $DevDependencies.Count -gt 0) { + Write-Host "Updating package.json dependencies..." -ForegroundColor Gray + $packageJson = Get-Content $packageJsonPath -Raw | ConvertFrom-Json + + foreach ($dep in $Dependencies.GetEnumerator()) { + if ($packageJson.dependencies -and $packageJson.dependencies.PSObject.Properties[$dep.Key]) { + $packageJson.dependencies.($dep.Key) = "file:$($dep.Value)" + } + } + + foreach ($dep in $DevDependencies.GetEnumerator()) { + if ($packageJson.devDependencies -and $packageJson.devDependencies.PSObject.Properties[$dep.Key]) { + $packageJson.devDependencies.($dep.Key) = "file:$($dep.Value)" + } + } + + $packageJson | ConvertTo-Json -Depth 100 | Set-Content $packageJsonPath -Encoding UTF8 + Write-Host " Updated dependencies to local packages" -ForegroundColor Green + } + + # Step 2: Install dependencies, clean, and build + Push-Location $GeneratorPath + try { + Write-Host "Installing dependencies..." -ForegroundColor Gray + if ($UseNpmCi) { + $installOutput = & npm install --package-lock-only 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host $installOutput -ForegroundColor Red + throw "Failed to update package-lock.json" + } + + $ciOutput = & npm ci 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host $ciOutput -ForegroundColor Red + throw "Failed to install dependencies" + } + } else { + $installOutput = & npm install 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host $installOutput -ForegroundColor Red + throw "Failed to run npm install" + } + } + + Write-Host "Cleaning build artifacts..." -ForegroundColor Gray + $cleanOutput = Invoke "npm run clean" $GeneratorPath + if ($LASTEXITCODE -ne 0) { + Write-Host $cleanOutput -ForegroundColor Red + throw "Failed to clean generator" + } + + Write-Host "Building generator..." -ForegroundColor Gray + $buildOutput = Invoke "npm run build" $GeneratorPath + if ($LASTEXITCODE -ne 0) { + Write-Host $buildOutput -ForegroundColor Red + throw "Failed to build generator" + } + + Write-Host " Build completed" -ForegroundColor Green + } + finally { + Pop-Location + } + + # Step 3: Package the generator + Write-Host "Packaging generator..." -ForegroundColor Gray + + # Update version in package.json for packaging + $packageJson = Get-Content $packageJsonPath -Raw | ConvertFrom-Json + $packageJson.version = $LocalVersion + $packageJson | ConvertTo-Json -Depth 100 | Set-Content $packageJsonPath -Encoding UTF8 + + Push-Location $GeneratorPath + try { + $packOutput = Invoke "npm pack" $GeneratorPath + if ($LASTEXITCODE -ne 0) { + Write-Host $packOutput -ForegroundColor Red + throw "Failed to pack generator" + } + + # Get the package filename + $packageLine = ($packOutput | Where-Object { $_ -match '\.tgz$' } | Select-Object -First 1).ToString().Trim() + if ($packageLine -match 'filename:\s*(.+\.tgz)') { + $packageFile = $Matches[1].Trim() + } else { + $packageFile = $packageLine + } + + # Move to debug folder + $sourcePath = Join-Path $GeneratorPath $packageFile + $destPath = Join-Path $DebugFolder $packageFile + Move-Item $sourcePath $destPath -Force + + Write-Host " Package created: $packageFile" -ForegroundColor Green + + return $destPath + } + finally { + Pop-Location + } + } + finally { + # Always restore original package.json + Set-Content $packageJsonPath $originalPackageJson -Encoding utf8 -NoNewline + } +} + +function Update-MgmtGenerator { + <# + .SYNOPSIS + Updates and builds the management plane generator (@azure-typespec/http-client-csharp-mgmt). + + .DESCRIPTION + This function handles the management plane generator setup: + 1. Updates package.json with local unbranded and Azure generator dependencies + 2. Runs npm install + 3. Runs npm run clean to ensure a clean build + 4. Builds the management plane generator + 5. Packages the management plane generator + 6. Updates eng folder emitter package artifacts (azure-typespec-http-client-csharp-mgmt-emitter-package.json) + 7. Updates mgmt emitter package artifact's devDependency for @typespec/http-client-csharp + + This function is designed to be called from RegenPreview.ps1 and uses the same + versioning scheme as the main generators. It derives all necessary paths from the + EngFolder parameter. + + .PARAMETER EngFolder + The eng folder path in azure-sdk-for-net. All other paths (mgmt generator, + package paths, Packages.Data.props) are derived from this. + + .PARAMETER DebugFolder + The debug folder path where the packaged generators (.tgz files) are located. + + .PARAMETER LocalVersion + The version string to use for the local package (e.g., "1.0.0-alpha.20250127.abc123"). + #> + param( + [Parameter(Mandatory=$true)] + [string]$EngFolder, + + [Parameter(Mandatory=$true)] + [string]$DebugFolder, + + [Parameter(Mandatory=$true)] + [string]$LocalVersion + ) + + $ErrorActionPreference = 'Stop' + + # Derive all paths from EngFolder + $mgmtGeneratorPath = Join-Path $EngFolder "packages" "http-client-csharp-mgmt" + + # Package paths come from debug folder + $azurePackageName = "azure-typespec-http-client-csharp-$LocalVersion.tgz" + $unbrandedPackageName = "typespec-http-client-csharp-$LocalVersion.tgz" + $azurePackagePath = Join-Path $DebugFolder $azurePackageName + $unbrandedPackagePath = Join-Path $DebugFolder $unbrandedPackageName + + if (-not (Test-Path $azurePackagePath)) { + throw "Azure package not found: $azurePackagePath" + } + if (-not (Test-Path $unbrandedPackagePath)) { + throw "Unbranded package not found: $unbrandedPackagePath" + } + + Write-Host "Management plane generator path: $mgmtGeneratorPath" -ForegroundColor Gray + Write-Host "Azure package: $azurePackagePath" -ForegroundColor Gray + Write-Host "Unbranded package: $unbrandedPackagePath" -ForegroundColor Gray + Write-Host "Local version: $LocalVersion" -ForegroundColor Gray + Write-Host "" + + # Use shared helper to build and package the mgmt generator + $mgmtPackagePath = Update-GeneratorPackage ` + -GeneratorPath $mgmtGeneratorPath ` + -Dependencies @{ '@azure-typespec/http-client-csharp' = $azurePackagePath } ` + -DevDependencies @{ '@typespec/http-client-csharp' = $unbrandedPackagePath } ` + -LocalVersion $LocalVersion ` + -DebugFolder $DebugFolder ` + -UseNpmCi $false + + # Update eng folder mgmt emitter package artifacts + Write-Host "Updating mgmt emitter package artifacts..." -ForegroundColor Gray + + # First, update the mgmt emitter package artifact's devDependency for @typespec/http-client-csharp + $mgmtEmitterJson = Join-Path $EngFolder "azure-typespec-http-client-csharp-mgmt-emitter-package.json" + $mgmtEmitterPackageJson = Get-Content $mgmtEmitterJson -Raw | ConvertFrom-Json + + # Check if devDependencies exists and has @typespec/http-client-csharp + if ($mgmtEmitterPackageJson.devDependencies -and + $mgmtEmitterPackageJson.devDependencies.PSObject.Properties['@typespec/http-client-csharp']) { + # Use file path to the unbranded package, not just the version string + $mgmtEmitterPackageJson.devDependencies.'@typespec/http-client-csharp' = "file:$unbrandedPackagePath" + $mgmtEmitterPackageJson | ConvertTo-Json -Depth 100 | Set-Content $mgmtEmitterJson -Encoding UTF8 + Write-Host " Updated @typespec/http-client-csharp devDependency to file:$unbrandedPackagePath" -ForegroundColor Green + } + + # Now update the package-lock.json with both dependencies + $tempDir = Join-Path $EngFolder "temp-mgmt-package-update" + New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + + try { + $tempPackageJson = Join-Path $tempDir "package.json" + + Copy-Item $mgmtEmitterJson $tempPackageJson -Force + + Push-Location $tempDir + try { + # Install the mgmt package and regenerate lock file with both dependencies + Invoke "npm install `"`"file:$mgmtPackagePath`"`" --package-lock-only" $tempDir + + Copy-Item $tempPackageJson $mgmtEmitterJson -Force + $lockFile = Join-Path $tempDir "package-lock.json" + if (Test-Path $lockFile) { + $mgmtLockJson = Join-Path $EngFolder "azure-typespec-http-client-csharp-mgmt-emitter-package-lock.json" + Copy-Item $lockFile $mgmtLockJson -Force + } + + Write-Host " Mgmt emitter package artifacts updated" -ForegroundColor Green + } + finally { + Pop-Location + } + } + finally { + Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + + Write-Host "" + + # Return the package path for potential further use + return $mgmtPackagePath +} + +function Update-AzureGenerator { + <# + .SYNOPSIS + Updates and builds the Azure generator (@azure-typespec/http-client-csharp). + + .DESCRIPTION + This function handles the Azure generator setup: + 1. Updates package.json to use local unbranded generator dependency + 2. Runs npm clean, install, and build + 3. Packages the Azure generator + 4. Builds and packages the Azure.Generator NuGet package + 5. Updates Packages.Data.props with AzureGeneratorVersion + + This function is designed to be called from RegenPreview.ps1. + + .PARAMETER AzureGeneratorPath + Path to the Azure generator directory in azure-sdk-for-net. + + .PARAMETER UnbrandedPackagePath + Path to the local unbranded TypeSpec emitter package (.tgz). + + .PARAMETER DebugFolder + The debug folder path where packaged artifacts will be stored. + + .PARAMETER PackagesDataPropsPath + Path to the Packages.Data.props file. + + .PARAMETER LocalVersion + The version string to use for the local package (e.g., "1.0.0-alpha.20250127.abc123"). + #> + param( + [Parameter(Mandatory=$true)] + [string]$AzureGeneratorPath, + + [Parameter(Mandatory=$true)] + [string]$UnbrandedPackagePath, + + [Parameter(Mandatory=$true)] + [string]$DebugFolder, + + [Parameter(Mandatory=$true)] + [string]$PackagesDataPropsPath, + + [Parameter(Mandatory=$true)] + [string]$LocalVersion + ) + + $ErrorActionPreference = 'Stop' + + Write-Host "Azure generator path: $AzureGeneratorPath" -ForegroundColor Gray + Write-Host "Unbranded package: $UnbrandedPackagePath" -ForegroundColor Gray + Write-Host "Local version: $LocalVersion" -ForegroundColor Gray + Write-Host "" + + # Use shared helper to build and package the Azure generator + $azurePackagePath = Update-GeneratorPackage ` + -GeneratorPath $AzureGeneratorPath ` + -Dependencies @{ '@typespec/http-client-csharp' = $UnbrandedPackagePath } ` + -LocalVersion $LocalVersion ` + -DebugFolder $DebugFolder ` + -UseNpmCi $true + + # Build and package Azure.Generator NuGet package + Write-Host "Packing Azure.Generator NuGet package..." -ForegroundColor Gray + + $azureGeneratorCsprojPath = Join-Path $AzureGeneratorPath "generator" "Azure.Generator" "src" "Azure.Generator.csproj" + if (-not (Test-Path $azureGeneratorCsprojPath)) { + throw "Azure.Generator project not found at: $azureGeneratorCsprojPath" + } + $packCmd = "dotnet pack `"$azureGeneratorCsprojPath`" /p:Version=$LocalVersion /p:PackageVersion=$LocalVersion /p:PackageOutputPath=`"$DebugFolder`" /p:HasReleaseVersion=`"false`" --configuration Debug --no-build --nologo -v:quiet" + Invoke $packCmd $AzureGeneratorPath + if ($LASTEXITCODE -ne 0) { + throw "Failed to pack Azure.Generator" + } + + Write-Host " Azure.Generator NuGet package created" -ForegroundColor Green + + # Update Packages.Data.props with Azure generator version + Write-Host "Updating Packages.Data.props..." -ForegroundColor Gray + $propsContent = Get-Content $PackagesDataPropsPath -Raw + $pattern = '()([^<]+)()' + + if ($propsContent -match $pattern) { + $oldVersion = $Matches[2] + $newContent = $propsContent -replace $pattern, "$LocalVersion" + Set-Content $PackagesDataPropsPath -Value $newContent -Encoding utf8 -NoNewline + Write-Host " Updated AzureGeneratorVersion from $oldVersion to $LocalVersion" -ForegroundColor Green + } else { + throw "AzureGeneratorVersion property not found in $PackagesDataPropsPath" + } + + Write-Host "" + + # Return the package path for potential further use + return $azurePackagePath +} + +function Filter-LibrariesByGenerator { + <# + .SYNOPSIS + Filters libraries based on generator type. + + .DESCRIPTION + This function filters a list of libraries based on the specified generator type. + Each library object should have a 'Generator' property. + If no generator filter is specified, all libraries are returned. + + .PARAMETER Libraries + Array of library objects to filter. Each object should have a 'Generator' property. + + .PARAMETER Azure + Filter for Azure generator (@azure-typespec/http-client-csharp). + + .PARAMETER Unbranded + Filter for unbranded generator (@typespec/http-client-csharp). + + .PARAMETER Mgmt + Filter for management plane generator (@azure-typespec/http-client-csharp-mgmt). + #> + param( + [Parameter(Mandatory=$true)] + [array]$Libraries, + + [Parameter(Mandatory=$false)] + [switch]$Azure, + + [Parameter(Mandatory=$false)] + [switch]$Unbranded, + + [Parameter(Mandatory=$false)] + [switch]$Mgmt + ) + + $ErrorActionPreference = 'Stop' + + # If no filters specified, return everything as an array + if (-not $Azure -and -not $Unbranded -and -not $Mgmt) { + return @($Libraries) + } + + # Filter based on specified generator type + $filtered = [System.Collections.ArrayList]::new() + + if ($Azure) { + $azureLibs = @($Libraries | Where-Object { $_.Generator -eq "@azure-typespec/http-client-csharp" }) + foreach ($lib in $azureLibs) { + [void]$filtered.Add($lib) + } + } + + if ($Unbranded) { + $unbrandedLibs = @($Libraries | Where-Object { $_.Generator -eq "@typespec/http-client-csharp" }) + foreach ($lib in $unbrandedLibs) { + [void]$filtered.Add($lib) + } + } + + if ($Mgmt) { + $mgmtLibs = @($Libraries | Where-Object { $_.Generator -eq "@azure-typespec/http-client-csharp-mgmt" }) + foreach ($lib in $mgmtLibs) { + [void]$filtered.Add($lib) + } + } + + # Return as array to ensure Count property is always available + return @($filtered.ToArray()) +} + +function Update-OpenAIGenerator { + <# + .SYNOPSIS + Updates and regenerates the OpenAI .NET library using local generators. + + .DESCRIPTION + This function handles the OpenAI generator workflow: + 1. Updates codegen/nuget.config with local NuGet package source + 2. Updates codegen/package.json with local unbranded generator dependency + 3. Updates OpenAI.Library.Plugin.csproj with local NuGet package version + 4. Invokes Invoke-CodeGen.ps1 with Clean option to regenerate the library + + This function is designed to be called from RegenPreview.ps1 for OpenAI repositories. + + .PARAMETER OpenAIRepoPath + Path to the openai-dotnet repository root. + + .PARAMETER UnbrandedPackagePath + Path to the local unbranded TypeSpec emitter package (.tgz). + + .PARAMETER LocalVersion + The version string to use for the local package (e.g., "1.0.0-alpha.20250127.abc123"). + + .PARAMETER DebugFolder + The debug folder path where NuGet packages are located. + #> + param( + [Parameter(Mandatory=$true)] + [string]$OpenAIRepoPath, + + [Parameter(Mandatory=$true)] + [string]$UnbrandedPackagePath, + + [Parameter(Mandatory=$true)] + [string]$LocalVersion, + + [Parameter(Mandatory=$true)] + [string]$DebugFolder + ) + + $ErrorActionPreference = 'Stop' + + Write-Host "OpenAI repository path: $OpenAIRepoPath" -ForegroundColor Gray + Write-Host "Unbranded package: $UnbrandedPackagePath" -ForegroundColor Gray + Write-Host "Local version: $LocalVersion" -ForegroundColor Gray + Write-Host "" + + # Update root nuget.config (not codegen/nuget.config) + # OpenAI uses packageSourceMapping, so we need to add patterns for the generator packages + $nugetConfigPath = Join-Path $OpenAIRepoPath "nuget.config" + Add-LocalNuGetSource ` + -NuGetConfigPath $nugetConfigPath ` + -SourcePath $DebugFolder ` + -PackagePatterns @( + "Microsoft.TypeSpec.Generator.ClientModel" + ) + + # Update codegen/package.json + $codegenPackageJsonPath = Join-Path $OpenAIRepoPath "codegen" "package.json" + if (-not (Test-Path $codegenPackageJsonPath)) { + throw "OpenAI codegen package.json not found: $codegenPackageJsonPath" + } + + Write-Host "Updating OpenAI codegen package.json..." -ForegroundColor Gray + $originalPackageJson = Get-Content $codegenPackageJsonPath -Raw + + try { + $packageJson = $originalPackageJson | ConvertFrom-Json + + # Update dependencies or devDependencies with local unbranded generator + $updated = $false + + if ($packageJson.dependencies -and $packageJson.dependencies.PSObject.Properties['@typespec/http-client-csharp']) { + $packageJson.dependencies.'@typespec/http-client-csharp' = "file:$UnbrandedPackagePath" + $updated = $true + Write-Host " Updated @typespec/http-client-csharp in dependencies to local package" -ForegroundColor Green + } + + if ($packageJson.devDependencies -and $packageJson.devDependencies.PSObject.Properties['@typespec/http-client-csharp']) { + $packageJson.devDependencies.'@typespec/http-client-csharp' = "file:$UnbrandedPackagePath" + $updated = $true + Write-Host " Updated @typespec/http-client-csharp in devDependencies to local package" -ForegroundColor Green + } + + if (-not $updated) { + throw "@typespec/http-client-csharp not found in dependencies or devDependencies" + } + + $packageJson | ConvertTo-Json -Depth 100 | Set-Content $codegenPackageJsonPath -Encoding UTF8 + + # Delete package-lock.json to force regeneration with new dependencies + $packageLockPath = Join-Path $OpenAIRepoPath "codegen" "package-lock.json" + if (Test-Path $packageLockPath) { + Write-Host "Deleting codegen/package-lock.json..." -ForegroundColor Gray + Remove-Item $packageLockPath -Force + } + + # Run npm install to regenerate package-lock.json with local dependencies + Write-Host "Installing dependencies in codegen directory..." -ForegroundColor Gray + Push-Location (Join-Path $OpenAIRepoPath "codegen") + try { + $npmOutput = Invoke "npm install" (Join-Path $OpenAIRepoPath "codegen") + if ($LASTEXITCODE -ne 0) { + Write-Host $npmOutput -ForegroundColor Red + throw "npm install failed in codegen directory" + } + Write-Host " Dependencies installed" -ForegroundColor Green + } + finally { + Pop-Location + } + + # Update OpenAI.Library.Plugin.csproj + $pluginCsprojPath = Join-Path $OpenAIRepoPath "codegen" "generator" "src" "OpenAI.Library.Plugin.csproj" + if (-not (Test-Path $pluginCsprojPath)) { + throw "OpenAI.Library.Plugin.csproj not found: $pluginCsprojPath" + } + + Write-Host "Updating OpenAI.Library.Plugin.csproj..." -ForegroundColor Gray + [xml]$csproj = Get-Content $pluginCsprojPath + + $packageReference = $csproj.Project.ItemGroup.PackageReference | + Where-Object { $_.Include -eq "Microsoft.TypeSpec.Generator.ClientModel" } | + Select-Object -First 1 + + if ($packageReference) { + $packageReference.Version = $LocalVersion + $csproj.Save($pluginCsprojPath) + Write-Host " Updated Microsoft.TypeSpec.Generator.ClientModel to $LocalVersion" -ForegroundColor Green + } else { + throw "Microsoft.TypeSpec.Generator.ClientModel package reference not found in csproj" + } + + # Invoke Invoke-CodeGen.ps1 with Clean option + $codeGenScript = Join-Path $OpenAIRepoPath "scripts" "Invoke-CodeGen.ps1" + if (-not (Test-Path $codeGenScript)) { + throw "Invoke-CodeGen.ps1 not found: $codeGenScript" + } + + Write-Host "Generating OpenAI library code..." -ForegroundColor Gray + Push-Location $OpenAIRepoPath + try { + $output = & $codeGenScript -Clean 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host $output -ForegroundColor Red + throw "OpenAI code generation failed" + } + Write-Host " OpenAI library regenerated successfully" -ForegroundColor Green + + # Store output for return + $generationOutput = $output -join "`n" + } + finally { + Pop-Location + } + + # On successful regeneration, restore all modified artifacts + Write-Host "Restoring modified artifacts..." -ForegroundColor Gray + Push-Location $OpenAIRepoPath + try { + $filesToRestore = @( + "codegen/package.json" + "package-lock.json" + "nuget.config" + "codegen/generator/src/OpenAI.Library.Plugin.csproj" + ) + $restoreCmd = "git restore $($filesToRestore -join ' ')" + Invoke $restoreCmd $OpenAIRepoPath + + Write-Host " All artifacts restored" -ForegroundColor Green + } + catch { + Write-Warning "Failed to restore some artifacts: $_" + } + finally { + Pop-Location + } + } + finally { + Pop-Location + } + + Write-Host "" + + # Return the generation output for reporting + return $generationOutput +} + +function Add-LocalNuGetSource { + <# + .SYNOPSIS + Adds a local NuGet package source to NuGet.Config. + + .DESCRIPTION + This function adds or updates a local NuGet package source in the repository's NuGet.Config file. + The source is inserted after the element if present, or appended otherwise. + + If PackagePatterns is provided, it also updates packageSourceMapping to allow the specified + packages to be loaded from the local source. This is required for repos that use package source mapping. + + This is a shared helper used by both Azure SDK and OpenAI workflows. + + .PARAMETER NuGetConfigPath + Path to the NuGet.Config file. + + .PARAMETER SourcePath + Path to the local NuGet package source directory. + + .PARAMETER SourceKey + The key/name for the NuGet source (default: "local-codegen-debug-packages"). + + .PARAMETER PackagePatterns + Optional array of package patterns to add to packageSourceMapping for this source. + Example: @("Microsoft.TypeSpec.Generator*", "Azure.Generator") + #> + param( + [Parameter(Mandatory=$true)] + [string]$NuGetConfigPath, + + [Parameter(Mandatory=$true)] + [string]$SourcePath, + + [Parameter(Mandatory=$false)] + [string]$SourceKey = "local-codegen-debug-packages", + + [Parameter(Mandatory=$false)] + [string[]]$PackagePatterns = @() + ) + + $ErrorActionPreference = 'Stop' + + if (-not (Test-Path $NuGetConfigPath)) { + throw "NuGet.Config not found at: $NuGetConfigPath" + } + + Write-Host "Adding local NuGet package source to NuGet.Config..." -ForegroundColor Gray + [xml]$nugetConfig = Get-Content $NuGetConfigPath + + # Ensure configuration element exists + if (-not $nugetConfig.configuration) { + throw "Invalid NuGet.Config: missing element" + } + + # Ensure packageSources element exists, create if needed + $packageSources = $nugetConfig.configuration.packageSources + if (-not $packageSources) { + $packageSources = $nugetConfig.CreateElement("packageSources") + $nugetConfig.configuration.AppendChild($packageSources) | Out-Null + } + + # Create local source element + $localSource = $nugetConfig.CreateElement("add") + $localSource.SetAttribute("key", $SourceKey) + $localSource.SetAttribute("value", $SourcePath) + + # Find the element and insert after it + $clearElement = $packageSources.ChildNodes | Where-Object { $_.Name -eq "clear" } | Select-Object -First 1 + + if ($clearElement -and $clearElement.NextSibling) { + $packageSources.InsertBefore($localSource, $clearElement.NextSibling) | Out-Null + } elseif ($clearElement) { + $packageSources.AppendChild($localSource) | Out-Null + } else { + # No clear element, just prepend to be first + if ($packageSources.FirstChild) { + $packageSources.InsertBefore($localSource, $packageSources.FirstChild) | Out-Null + } else { + $packageSources.AppendChild($localSource) | Out-Null + } + } + + Write-Host " Added local NuGet source: $SourceKey" -ForegroundColor Green + + # Handle packageSourceMapping if PackagePatterns are provided + if ($PackagePatterns.Count -gt 0) { + Write-Host " Updating packageSourceMapping..." -ForegroundColor Gray + + $packageSourceMapping = $nugetConfig.configuration.packageSourceMapping + if (-not $packageSourceMapping) { + $packageSourceMapping = $nugetConfig.CreateElement("packageSourceMapping") + $nugetConfig.configuration.AppendChild($packageSourceMapping) | Out-Null + } + + # Find or create packageSource element for our local source + $localPackageSource = $packageSourceMapping.packageSource | Where-Object { $_.key -eq $SourceKey } | Select-Object -First 1 + + if (-not $localPackageSource) { + $localPackageSource = $nugetConfig.CreateElement("packageSource") + $localPackageSource.SetAttribute("key", $SourceKey) + + # Insert at the beginning of packageSourceMapping (highest priority) + if ($packageSourceMapping.FirstChild) { + $packageSourceMapping.InsertBefore($localPackageSource, $packageSourceMapping.FirstChild) | Out-Null + } else { + $packageSourceMapping.AppendChild($localPackageSource) | Out-Null + } + } + + # Add package patterns + foreach ($pattern in $PackagePatterns) { + $packageElement = $nugetConfig.CreateElement("package") + $packageElement.SetAttribute("pattern", $pattern) + $localPackageSource.AppendChild($packageElement) | Out-Null + Write-Host " Added pattern: $pattern" -ForegroundColor Green + } + } + + $nugetConfig.Save($NuGetConfigPath) +} + +Export-ModuleMember -Function "Update-MgmtGenerator", "Update-AzureGenerator", "Filter-LibrariesByGenerator", "Update-OpenAIGenerator", "Add-LocalNuGetSource" diff --git a/packages/http-client-csharp/eng/scripts/docs/RegenPreview.md b/packages/http-client-csharp/eng/scripts/docs/RegenPreview.md new file mode 100644 index 00000000000..38092a1b997 --- /dev/null +++ b/packages/http-client-csharp/eng/scripts/docs/RegenPreview.md @@ -0,0 +1,439 @@ +# RegenPreview.ps1 + +## Overview + +`RegenPreview.ps1` is a PowerShell script that automates the process of building local generator packages and regenerating libraries for validation purposes. This script supports two modes: + +- **Azure SDK Mode**: Regenerate Azure SDK for .NET libraries +- **OpenAI Mode**: Regenerate the OpenAI .NET library + +This script is designed to streamline the workflow for testing changes to the TypeSpec HTTP Client C# generator before submitting a pull request. + +## Purpose + +When making changes to the TypeSpec HTTP Client C# generator, it may be helpful to validate what the effects will be to generated libraries. This script automates the entire validation workflow by: + +### Azure SDK Mode + +1. Building local versions of the generator packages +2. Updating the Azure SDK for .NET repository to use these local packages +3. Regenerating all libraries (or selected libraries if `-Select` is specified) +4. Cleaning up all modifications after validation + +### OpenAI Mode + +1. Building local version of the unbranded generator package +2. Building local NuGet framework packages +3. Updating the OpenAI .NET repository's codegen configuration +4. Regenerating the OpenAI library using Invoke-CodeGen.ps1 +5. Cleaning up all modifications after validation + +## Prerequisites + +- **PowerShell 7.0 or later** (cross-platform: Windows, Linux, macOS) +- Node.js and npm installed +- .NET SDK installed +- Local clone of the `typespec` repository (unbranded generator) +- Local clone of the `azure-sdk-for-net` repository (for Azure SDK mode) +- Local clone of the `openai-dotnet` repository (for OpenAI mode) + +## Usage + +### Azure SDK Mode - Basic Usage (Regenerate All) + +**Windows:** + +```powershell +.\RegenPreview.ps1 -SdkLibraryRepoPath "C:\path\to\azure-sdk-for-net" +``` + +**Linux/macOS:** + +```powershell +./RegenPreview.ps1 -SdkLibraryRepoPath "/home/user/repos/azure-sdk-for-net" +``` + +This will: + +- Clean and build the unbranded and Azure generators with local changes +- Regenerate **all** libraries automatically +- Restore all modified metadata files on success (ie. package.json, package-lock.json) + +### OpenAI Mode - Basic Usage + +**Windows:** + +```powershell +.\RegenPreview.ps1 -SdkLibraryRepoPath "C:\path\to\openai-dotnet" +``` + +**Linux/macOS:** + +```powershell +./RegenPreview.ps1 -SdkLibraryRepoPath "/home/user/repos/openai-dotnet" +``` + +This will: + +- Clean and build the unbranded generator with local changes +- Build and package NuGet framework packages +- Update root nuget.config with local NuGet source and packageSourceMapping +- Update codegen/package.json with local generator +- Regenerate codegen/package-lock.json with npm install +- Update OpenAI.Library.Plugin.csproj with local NuGet packages +- Regenerate the OpenAI library +- Restore all modified generator metadata files on success + +**Note**: OpenAI mode is automatically detected when the repository path contains "openai-dotnet". The `-Select`, `-Azure`, `-Unbranded`, and `-Mgmt` parameters are not applicable in OpenAI mode. + +### Parameters + +#### `-SdkLibraryRepoPath` (Required) + +The local file system path to your SDK repository (`azure-sdk-for-net` or `openai-dotnet`). When the path contains "openai-dotnet", the script automatically operates in OpenAI mode. + +**Windows Example:** + +```powershell +-SdkLibraryRepoPath "C:\repos\azure-sdk-for-net" +``` + +**Linux/macOS Example:** + +```powershell +-SdkLibraryRepoPath "/home/user/repos/azure-sdk-for-net" +``` + +#### `-Select` (Optional) + +**Azure SDK Mode only.** When specified, displays an interactive menu to select specific libraries to regenerate. If omitted, all libraries are regenerated automatically. + +Can be combined with generator filter parameters (`-Azure`, `-Unbranded`, `-Mgmt`) to interactively select from a filtered subset. + +Not applicable in OpenAI mode. + +**Example:** + +```powershell +.\RegenPreview.ps1 -SdkLibraryRepoPath "C:\repos\azure-sdk-for-net" -Select +``` + +#### `-Azure` (Optional) + +**Azure SDK Mode only.** When specified, only regenerates libraries using the Azure-branded generator (`@azure-typespec/http-client-csharp`). + +Not applicable in OpenAI mode. + +**Example:** + +```powershell +.\RegenPreview.ps1 -SdkLibraryRepoPath "C:\repos\azure-sdk-for-net" -Azure +``` + +#### `-Unbranded` (Optional) + +**Azure SDK Mode only.** When specified, only regenerates libraries using the unbranded generator (`@typespec/http-client-csharp`). +Not applicable in OpenAI mode. + +**Example:** + +```powershell +.\RegenPreview.ps1 -SdkLibraryRepoPath "C:\repos\azure-sdk-for-net" -Unbranded +``` + +#### `-Mgmt` (Optional) + +**Azure SDK Mode only.** When specified, only regenerates libraries using the management plane generator (`@azure-typespec/http-client-csharp-mgmt`). + +If no generator filter parameter is specified, all libraries are regenerated by default. + +Not applicable in OpenAI mode. + +**Example:** + +```powershell +.\RegenPreview.ps1 -SdkLibraryRepoPath "C:\repos\azure-sdk-for-net" -Mgmt +``` + +### Interactive Selection + +When running with the `-Select` flag, the script presents an interactive menu listing all available libraries: + +``` +==================== LIBRARY SELECTION ==================== +Found 28 libraries available for regeneration + +Azure-branded libraries (@azure-typespec/http-client-csharp): + [ 1] Azure.AI.VoiceLive (ai) + [ 2] Azure.Data.AppConfiguration (appconfiguration) + ... + +Unbranded libraries (@typespec/http-client-csharp): + [12] Azure.AI.Projects (ai) + [13] Azure.AI.OpenAI (openai) + +Management plane libraries (@azure-typespec/http-client-csharp-mgmt): + [14] Azure.ResourceManager.AgriculturePlatform (agricultureplatform) + [15] Azure.ResourceManager.ArizeAIObservabilityEval (arizeaiobservabilityeval) + ... + +Enter library numbers to regenerate (comma-separated), 'all' for all libraries, or 'q' to quit: +Example: 1,3,5 or 1-4,7 or all +``` + +**Selection Options:** + +- Single library: `1` +- Multiple libraries: `1,3,5` +- Range of libraries: `1-4` +- Combination: `1-4,7,10` +- All libraries: `all` +- Quit: `q` + +## How It Works + +The script operates in two distinct modes based on the repository path provided. + +### Azure SDK Mode + +The script performs the following steps in sequence when the repository path points to `azure-sdk-for-net`: + +### Step 0: Library Selection (Interactive Mode Only) + +- Parses `Library_Inventory.md` in the azure-sdk-for-net repository +- Applies any generator filters if specified (`-Azure`, `-Unbranded`, `-Mgmt`) +- Displays available libraries grouped by generator type +- Prompts user to select which libraries to regenerate +- Skipped when `-Select` is not specified (non-interactive mode) + +### Step 1: Build Unbranded Generator + +- Runs `npm ci` to install dependencies +- Runs `npm run clean` to ensure a clean build +- Runs `npm run build` to build `@typespec/http-client-csharp` + +### Step 2: Package Unbranded Generator + +- Generates a local version string: `1.0.0-alpha.YYYYMMDD.hash` +- Updates `package.json` with the local version +- Runs `npm pack` to create a `.tgz` package +- Moves the package to the `debug` folder +- Restores original `package.json` + +### Step 2.5: Build and Package NuGet Packages + +- Generates NuGet packages for three generator framework projects: + - `Microsoft.TypeSpec.Generator` + - `Microsoft.TypeSpec.Generator.Input` + - `Microsoft.TypeSpec.Generator.ClientModel` +- Uses Debug configuration with `--no-build` (reuses binaries from Step 1) +- Outputs `.nupkg` files to the `debug` folder +- In Azure SDK mode: Updates `Packages.Data.props` and `NuGet.Config` +- In OpenAI mode: Proceeds to Step 3 (OpenAI regeneration) + +--- + +### OpenAI Mode + +When the repository path contains "openai-dotnet", the script switches to OpenAI mode and performs the following steps after Step 2.5: + +#### Step 3: Update OpenAI Generator and Regenerate + +**What the script does:** + +- Updates `nuget.config` (at the root of the repo) with local NuGet package source + - Adds the local debug folder as a package source + - Updates `packageSourceMapping` to allow Generator packages from the local source (required for OpenAI's package source mapping configuration) +- Updates `codegen/package.json` with local unbranded generator dependency (`@typespec/http-client-csharp`) + - Checks both `dependencies` and `devDependencies` sections for the package + - Updates whichever section(s) contain the package to use `file:` protocol pointing to local `.tgz` +- Deletes `codegen/package-lock.json` to force regeneration with new dependencies +- Runs `npm install` in the codegen directory to regenerate the lock file with local dependencies +- Updates `codegen/generator/src/OpenAI.Library.Plugin.csproj` with local NuGet package version for `Microsoft.TypeSpec.Generator.ClientModel` +- Invokes `scripts/Invoke-CodeGen.ps1 -Clean` to regenerate the OpenAI library +- **On successful regeneration**, restores all modified artifacts: + - `codegen/package.json` + - `codegen/package-lock.json` + - `nuget.config` + - `codegen/generator/src/OpenAI.Library.Plugin.csproj` +- **On failure**, leaves all modified artifacts in place for debugging +- Displays success message and exits + +**Note:** Filter parameters (`-Azure`, `-Unbranded`, `-Mgmt`, `-Select`) are not applicable in OpenAI mode and will cause an error if specified. + +--- + +### Azure SDK Mode (continued) + +In Azure SDK mode, the script continues with additional steps after Step 2.5: + +### Step 3: Update and Build Azure Generator + +- Updates the Azure generator's `package.json` to reference the local unbranded package using `file:` protocol +- Runs `npm run clean` to clear previous builds +- Runs `npm install --package-lock-only` to update the lock file with the new local dependency +- Runs `npm ci` for clean installation based on the updated lock file +- Builds the Azure generator with `npm run build` + +### Step 4: Package Azure Generator + +- Updates `package.json` with the local version (dependency already set in Step 3) +- Runs `npm pack` to create a `.tgz` package +- Moves the package to the `debug` folder +- Restores original `package.json` (after both Steps 3 and 4 complete) + +### Step 5: Update eng Folder Artifacts + +- Updates package.json artifacts in `azure-sdk-for-net/eng`: + - `azure-typespec-http-client-csharp-emitter-package.json` + - `azure-typespec-http-client-csharp-emitter-package-lock.json` + - `http-client-csharp-emitter-package.json` + - `http-client-csharp-emitter-package-lock.json` +- Uses a temporary directory to safely update the lock files +- These files control which generator versions are used during library regeneration + +### Step 6: Update and Build Management Plane Generator + +- Updates the management plane generator (`@azure-typespec/http-client-csharp-mgmt`) located in `azure-sdk-for-net/eng/packages/http-client-csharp-mgmt` +- Updates `package.json` to reference both: + - Local Azure generator (`@azure-typespec/http-client-csharp`) using `file:` protocol + - Local unbranded generator (`@typespec/http-client-csharp`) using `file:` protocol +- Runs `npm install` to install dependencies +- Runs `npm run clean` to ensure a clean build +- Runs `npm run build` to build the management plane generator +- Creates a local package with `npm pack` +- Updates eng folder emitter package artifacts: + - `azure-typespec-http-client-csharp-mgmt-emitter-package.json` + - `azure-typespec-http-client-csharp-mgmt-emitter-package-lock.json` +- Updates `Packages.Data.props` in azure-sdk-for-net with `AzureGeneratorVersion` property + - This version is used by management plane libraries that reference the `Azure.Generator` NuGet package +- This step is skipped if the management plane generator directory doesn't exist + +### Step 7: Prepare Library List + +- Confirms the list of libraries to regenerate +- When no `-Select` flag, loads all libraries from `Library_Inventory.md` including: + - Data plane libraries using `@azure-typespec/http-client-csharp` + - Data plane libraries using `@typespec/http-client-csharp` + - Management plane libraries using `@azure-typespec/http-client-csharp-mgmt` +- In interactive mode (`-Select`), uses the previously selected libraries + +### Step 8: Regenerate Libraries + +- **Pre-Install tsp-client**: Runs `npm ci --prefix eng\common\tsp-client` once before parallel execution + - Installs shared TypeSpec compiler tooling used by all libraries + - Eliminates concurrent npm conflicts during parallel regeneration +- **Parallel Execution**: Uses PowerShell's `ForEach-Object -Parallel` to regenerate multiple libraries concurrently +- **Cross-Platform CPU Detection**: + - Windows: Uses `Get-CimInstance` with `Win32_Processor` + - Linux: Uses `nproc` command + - macOS: Uses `sysctl -n hw.ncpu` command +- **Throttle Limit**: Automatically calculated as `(CPU cores - 2)` with a minimum of 1 and maximum of 8 +- **Skip Redundant Installs**: Each parallel job uses `/p:SkipTspClientInstall=true` to skip re-installing tsp-client +- **Thread-Safe Progress**: Real-time updates showing `[completed/total] ✓/✗ LibraryName` + - Success: Green with ✓ + - Failure: White with ✗ (red reserved for error details in final report) +- **Isolated Execution**: Each library regenerates in its own directory (no conflicts) +- Runs `dotnet build /t:GenerateCode /p:SkipTspClientInstall=true` in the library's project directory +- Automatically detects if the `.csproj` is in a `src` subdirectory +- Tracks success/failure for each library with colored output + +### Step 9: Restore Artifacts (On Success Only) + +If all libraries regenerate successfully, the script restores modified files: + +- `eng/azure-typespec-http-client-csharp-emitter-package.json` +- `eng/azure-typespec-http-client-csharp-emitter-package-lock.json` +- `eng/http-client-csharp-emitter-package.json` +- `eng/http-client-csharp-emitter-package-lock.json` +- `eng/azure-typespec-http-client-csharp-mgmt-emitter-package.json` +- `eng/azure-typespec-http-client-csharp-mgmt-emitter-package-lock.json` +- `eng/packages/http-client-csharp/package-lock.json` +- `eng/packages/http-client-csharp-mgmt/package.json` +- `eng/packages/http-client-csharp-mgmt/package-lock.json` +- `eng/Packages.Data.props` +- `NuGet.Config` + +**Note:** If any libraries fail, artifacts are NOT restored, allowing you to debug the issue with the modified configuration intact. + +### Error Handling + +If the script encounters an error during pre-requisite steps (Steps 1-6), it will: + +- Display the error message and stack trace +- Attempt to restore `NuGet.Config` to prevent leaving the repository in an inconsistent state +- Exit with code 1 + +## Output + +### Debug Folder + +All packaged artifacts are stored in the `debug` folder at the root of the unbranded generator: + +- `typespec-http-client-csharp-{version}.tgz` - Unbranded generator npm package +- `azure-typespec-http-client-csharp-{version}.tgz` - Azure generator npm package +- `Microsoft.TypeSpec.Generator.{version}.nupkg` - Core generator NuGet package +- `Microsoft.TypeSpec.Generator.Input.{version}.nupkg` - Input models NuGet package +- `Microsoft.TypeSpec.Generator.ClientModel.{version}.nupkg` - Client model NuGet package +- `regen-report.json` - Detailed JSON report of regeneration results + +### Console Output + +The script provides colored console output with: + +- **Cyan**: Step headers and section dividers +- **Yellow**: Important information (versions, library counts, warnings) +- **Gray**: Detailed command execution and file operations +- **Green**: Success messages +- **Red**: Error messages and failed libraries +- **White**: Library names during regeneration + +### Regeneration Report + +After regeneration completes, a summary report is displayed: + +``` +==================== REGENERATION REPORT ==================== +Total Libraries: 3 +Passed: 2 +Failed: 1 +Execution Time: 00:02:45 + +PASSED LIBRARIES: + ✓ Azure.AI.VoiceLive (ai) + ✓ Azure.AI.Projects (cognitiveservices) + +FAILED LIBRARIES: + ✗ Azure.Messaging.EventGrid.Namespaces (eventgrid) + Error: Generation failed with exit code 1 + Details: ... + +============================================================= +Detailed report saved to: C:\...\debug\regen-report.json +``` + +## Common Scenarios + +### Scenario 1: Test OpenAI Library Changes + +```powershell +# You made changes to the unbranded generator that affect OpenAI +# Regenerate the OpenAI library to validate your changes +.\RegenPreview.ps1 -SdkLibraryRepoPath "C:\repos\openai-dotnet" +``` + +### Scenario 2: Test a Small Change Quickly (Azure SDK) + +```powershell +# You want to test Azure generator changes on specific libraries only +.\RegenPreview.ps1 -SdkLibraryRepoPath "C:\repos\azure-sdk-for-net" -Select + +# Select just one or two libraries when prompted +# Selection: 1,5 +``` + +### Scenario: Full Validation Before PR + +```powershell +.\RegenPreview.ps1 -SdkLibraryRepoPath "C:\repos\azure-sdk-for-net" +```