<# .SYNOPSIS Builds a distributable .zip of the Studiou WC Mail Queue plugin. .DESCRIPTION Packages only the shippable runtime — PHP, views, assets, languages and the user-facing readme. Development artefacts are excluded: the docs/ tree (which includes a ~6,000-file vendored copy of WooCommerce), CLAUDE.md, this build script, dotfiles and any previous zips. Two layouts, because the plugin can run either way: -Type MuPlugin (default) Extract the zip straight into wp-content/mu-plugins/. The loader stub is placed at the archive root as studiou-wc-mail-queue.php, beside the studiou-wc-mail-queue/ directory — exactly the two-part install the plugin requires. No manual copying, so the "forgot the loader stub → silently inert" trap cannot happen. -Type Plugin A conventional single-folder plugin zip, uploadable via Plugins -> Add New -> Upload and installed to wp-content/plugins/. The loader stub is not needed there; the main file's own plugin header runs it. .PARAMETER OutputDir Where the .zip is written. Default: D:\@StudioU .PARAMETER Type MuPlugin (default) or Plugin. See above. .PARAMETER PluginDir Source plugin directory. Default: the folder this script lives in. .EXAMPLE .\build-package.ps1 Builds the mu-plugin package into D:\@StudioU. .EXAMPLE .\build-package.ps1 -Type Plugin Builds a conventional plugin zip instead. #> [CmdletBinding()] param( [string] $OutputDir = 'D:\@StudioU', [ValidateSet('MuPlugin', 'Plugin')] [string] $Type = 'MuPlugin', [string] $PluginDir ) $ErrorActionPreference = 'Stop' # --- Resolve the plugin directory ------------------------------------------ if ([string]::IsNullOrWhiteSpace($PluginDir)) { if ($PSScriptRoot) { $PluginDir = $PSScriptRoot } elseif ($MyInvocation.MyCommand.Path) { $PluginDir = Split-Path -Parent $MyInvocation.MyCommand.Path } else { $PluginDir = (Get-Location).Path } } $PluginDir = (Resolve-Path -LiteralPath $PluginDir).Path $Slug = 'studiou-wc-mail-queue' $MainFile = Join-Path $PluginDir "$Slug.php" $LoaderFile = Join-Path $PluginDir "loader\$Slug.php" $ScriptName = $MyInvocation.MyCommand.Name function Write-Step ($msg) { Write-Host " $msg" -ForegroundColor Cyan } function Write-Ok ($msg) { Write-Host " $msg" -ForegroundColor Green } function Write-Note ($msg) { Write-Host " $msg" -ForegroundColor DarkGray } Write-Host "" Write-Host "Studiou WC Mail Queue - package builder" -ForegroundColor White Write-Host "----------------------------------------" # --- Sanity checks ---------------------------------------------------------- if (-not (Test-Path -LiteralPath $MainFile)) { throw "Main plugin file not found: $MainFile`n(Is -PluginDir pointing at the plugin folder?)" } if ($Type -eq 'MuPlugin' -and -not (Test-Path -LiteralPath $LoaderFile)) { throw "Loader stub not found: $LoaderFile`n(Required for a MuPlugin package.)" } # --- Read the version from the plugin header -------------------------------- $header = Get-Content -LiteralPath $MainFile -Raw $version = $null if ($header -match '(?m)^\s*\*\s*Version:\s*(.+?)\s*$') { $version = $Matches[1].Trim() } if ([string]::IsNullOrWhiteSpace($version)) { throw "Could not read 'Version:' from the plugin header in $MainFile" } Write-Step "Version: $version" Write-Step "Type: $Type" Write-Step "Source: $PluginDir" # --- Warn if the compiled translation is missing ---------------------------- $moFiles = @(Get-ChildItem -LiteralPath (Join-Path $PluginDir 'languages') -Filter '*.mo' -ErrorAction SilentlyContinue) if ($moFiles.Count -eq 0) { Write-Warning "No compiled .mo in languages/. The admin UI renders in English until you compile it (Poedit / msgfmt)." } # --- Prepare output --------------------------------------------------------- if (-not (Test-Path -LiteralPath $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null Write-Note "Created output directory: $OutputDir" } if ($Type -eq 'Plugin') { $zipName = "$Slug-$version.plugin.zip" } else { $zipName = "$Slug-$version.zip" } $zipPath = Join-Path $OutputDir $zipName # --- Stage the files -------------------------------------------------------- $staging = Join-Path ([System.IO.Path]::GetTempPath()) ("studiou-wcmq-build-" + [guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $staging | Out-Null try { $pluginStage = Join-Path $staging $Slug # Directories excluded from the copy. `loader` is excluded for MuPlugin # because its stub is hoisted to the archive root; it is kept inside the # folder for a conventional Plugin package. $excludeDirs = @('docs', '.git', '.github', '.claude', '.vs', 'node_modules') if ($Type -eq 'MuPlugin') { $excludeDirs += 'loader' } # Files excluded by name / pattern. $excludeFiles = @('*.zip', '*.ps1', '.gitignore', '.gitattributes', 'CLAUDE.md', 'Thumbs.db', 'desktop.ini', '.DS_Store', $ScriptName) Write-Step "Staging files (excluding dev artefacts)..." # robocopy prunes excluded directories rather than copying then deleting, so # the huge vendored WooCommerce tree under docs/ is never even traversed. $roboArgs = @($PluginDir, $pluginStage, '/E', '/XD') + $excludeDirs + @('/XF') + $excludeFiles + @('/R:1', '/W:1', '/NFL', '/NDL', '/NJH', '/NJS', '/NP', '/NC', '/NS') robocopy @roboArgs | Out-Null # robocopy exit codes 0-7 are all success; 8+ is a real failure. if ($LASTEXITCODE -ge 8) { throw "robocopy failed with exit code $LASTEXITCODE while staging files." } $global:LASTEXITCODE = 0 if (-not (Test-Path -LiteralPath $MainFile.Replace($PluginDir, $pluginStage))) { throw "Staging did not produce the main plugin file - aborting." } # For a mu-plugin, place the loader stub at the archive root so extraction # into wp-content/mu-plugins/ needs no further steps. if ($Type -eq 'MuPlugin') { Copy-Item -LiteralPath $LoaderFile -Destination (Join-Path $staging "$Slug.php") Write-Note "Loader stub placed at archive root." } # --- Compress ----------------------------------------------------------- if (Test-Path -LiteralPath $zipPath) { Remove-Item -LiteralPath $zipPath -Force Write-Note "Replaced existing $zipName" } Write-Step "Compressing..." # NOT Compress-Archive: in Windows PowerShell 5.1 it writes entry paths with # backslashes, which violates the ZIP spec (APPNOTE mandates forward slashes). # On the Linux host that runs WordPress those extract as flat files with # literal backslashes in their names, so the plugin never installs. Build the # archive by hand and force forward-slash entry names. Add-Type -AssemblyName System.IO.Compression Add-Type -AssemblyName System.IO.Compression.FileSystem $stagingRoot = $staging.TrimEnd('\') $fs = [System.IO.File]::Open($zipPath, [System.IO.FileMode]::Create) $archive = New-Object System.IO.Compression.ZipArchive($fs, [System.IO.Compression.ZipArchiveMode]::Create) try { Get-ChildItem -LiteralPath $staging -Recurse -File | ForEach-Object { $rel = $_.FullName.Substring($stagingRoot.Length + 1).Replace('\', '/') [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile( $archive, $_.FullName, $rel, [System.IO.Compression.CompressionLevel]::Optimal) | Out-Null } } finally { $archive.Dispose() $fs.Dispose() } } finally { if (Test-Path -LiteralPath $staging) { Remove-Item -LiteralPath $staging -Recurse -Force -ErrorAction SilentlyContinue } } # --- Verify the archive ----------------------------------------------------- Write-Step "Verifying..." Add-Type -AssemblyName System.IO.Compression.FileSystem $zip = [System.IO.Compression.ZipFile]::OpenRead($zipPath) try { $entries = $zip.Entries $fileCount = ($entries | Where-Object { $_.Name -ne '' }).Count # Guard against dev artefacts leaking in. $leaked = $entries | Where-Object { $_.FullName -match '(^|/)docs/' -or $_.FullName -match '(^|/)\.git/' -or $_.FullName -match '(^|/)woocommerce/' -or $_.FullName -like '*CLAUDE.md' -or $_.FullName -like '*.ps1' } if ($leaked) { $names = ($leaked | Select-Object -First 5 | ForEach-Object { $_.FullName }) -join ', ' throw "Archive contains files that should have been excluded: $names" } # Confirm the entry point exists where install expects it. if ($Type -eq 'MuPlugin') { $needMain = $entries | Where-Object { $_.FullName -eq "$Slug/$Slug.php" } $needLoader = $entries | Where-Object { $_.FullName -eq "$Slug.php" } if (-not $needMain) { throw "Archive is missing $Slug/$Slug.php" } if (-not $needLoader) { throw "Archive is missing the root loader stub $Slug.php" } } else { $needMain = $entries | Where-Object { $_.FullName -eq "$Slug/$Slug.php" } if (-not $needMain) { throw "Archive is missing $Slug/$Slug.php" } } } finally { $zip.Dispose() } # --- Summary ---------------------------------------------------------------- $sizeKB = [math]::Round((Get-Item -LiteralPath $zipPath).Length / 1KB, 1) Write-Host "" Write-Ok "Built $zipName" Write-Note "Path: $zipPath" Write-Note "Size: $sizeKB KB" Write-Note "Files: $fileCount" Write-Host "" if ($Type -eq 'MuPlugin') { Write-Host "Install (mu-plugin):" -ForegroundColor White Write-Note "Extract the zip directly into wp-content/mu-plugins/ so you get:" Write-Note " wp-content/mu-plugins/$Slug.php (loader stub)" Write-Note " wp-content/mu-plugins/$Slug/ (plugin)" Write-Note "Then: WooCommerce -> Mail Queue -> Enable. Ships disabled." } else { Write-Host "Install (regular plugin):" -ForegroundColor White Write-Note "wp-admin -> Plugins -> Add New -> Upload Plugin -> choose this zip, then Activate." Write-Note "Then: WooCommerce -> Mail Queue -> Enable. Ships disabled." } Write-Host ""