build-package.ps1 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. <#
  2. .SYNOPSIS
  3. Builds a distributable .zip of the Studiou WC Mail Queue plugin.
  4. .DESCRIPTION
  5. Packages only the shippable runtime — PHP, views, assets, languages and the
  6. user-facing readme. Development artefacts are excluded: the docs/ tree (which
  7. includes a ~6,000-file vendored copy of WooCommerce), CLAUDE.md, this build
  8. script, dotfiles and any previous zips.
  9. Two layouts, because the plugin can run either way:
  10. -Type MuPlugin (default) Extract the zip straight into wp-content/mu-plugins/.
  11. The loader stub is placed at the archive root as
  12. studiou-wc-mail-queue.php, beside the
  13. studiou-wc-mail-queue/ directory — exactly the
  14. two-part install the plugin requires. No manual
  15. copying, so the "forgot the loader stub → silently
  16. inert" trap cannot happen.
  17. -Type Plugin A conventional single-folder plugin zip, uploadable
  18. via Plugins -> Add New -> Upload and installed to
  19. wp-content/plugins/. The loader stub is not needed
  20. there; the main file's own plugin header runs it.
  21. .PARAMETER OutputDir
  22. Where the .zip is written. Default: D:\@StudioU
  23. .PARAMETER Type
  24. MuPlugin (default) or Plugin. See above.
  25. .PARAMETER PluginDir
  26. Source plugin directory. Default: the folder this script lives in.
  27. .EXAMPLE
  28. .\build-package.ps1
  29. Builds the mu-plugin package into D:\@StudioU.
  30. .EXAMPLE
  31. .\build-package.ps1 -Type Plugin
  32. Builds a conventional plugin zip instead.
  33. #>
  34. [CmdletBinding()]
  35. param(
  36. [string] $OutputDir = 'D:\@StudioU',
  37. [ValidateSet('MuPlugin', 'Plugin')]
  38. [string] $Type = 'MuPlugin',
  39. [string] $PluginDir
  40. )
  41. $ErrorActionPreference = 'Stop'
  42. # --- Resolve the plugin directory ------------------------------------------
  43. if ([string]::IsNullOrWhiteSpace($PluginDir)) {
  44. if ($PSScriptRoot) {
  45. $PluginDir = $PSScriptRoot
  46. } elseif ($MyInvocation.MyCommand.Path) {
  47. $PluginDir = Split-Path -Parent $MyInvocation.MyCommand.Path
  48. } else {
  49. $PluginDir = (Get-Location).Path
  50. }
  51. }
  52. $PluginDir = (Resolve-Path -LiteralPath $PluginDir).Path
  53. $Slug = 'studiou-wc-mail-queue'
  54. $MainFile = Join-Path $PluginDir "$Slug.php"
  55. $LoaderFile = Join-Path $PluginDir "loader\$Slug.php"
  56. $ScriptName = $MyInvocation.MyCommand.Name
  57. function Write-Step ($msg) { Write-Host " $msg" -ForegroundColor Cyan }
  58. function Write-Ok ($msg) { Write-Host " $msg" -ForegroundColor Green }
  59. function Write-Note ($msg) { Write-Host " $msg" -ForegroundColor DarkGray }
  60. Write-Host ""
  61. Write-Host "Studiou WC Mail Queue - package builder" -ForegroundColor White
  62. Write-Host "----------------------------------------"
  63. # --- Sanity checks ----------------------------------------------------------
  64. if (-not (Test-Path -LiteralPath $MainFile)) {
  65. throw "Main plugin file not found: $MainFile`n(Is -PluginDir pointing at the plugin folder?)"
  66. }
  67. if ($Type -eq 'MuPlugin' -and -not (Test-Path -LiteralPath $LoaderFile)) {
  68. throw "Loader stub not found: $LoaderFile`n(Required for a MuPlugin package.)"
  69. }
  70. # --- Read the version from the plugin header --------------------------------
  71. $header = Get-Content -LiteralPath $MainFile -Raw
  72. $version = $null
  73. if ($header -match '(?m)^\s*\*\s*Version:\s*(.+?)\s*$') {
  74. $version = $Matches[1].Trim()
  75. }
  76. if ([string]::IsNullOrWhiteSpace($version)) {
  77. throw "Could not read 'Version:' from the plugin header in $MainFile"
  78. }
  79. Write-Step "Version: $version"
  80. Write-Step "Type: $Type"
  81. Write-Step "Source: $PluginDir"
  82. # --- Warn if the compiled translation is missing ----------------------------
  83. $moFiles = @(Get-ChildItem -LiteralPath (Join-Path $PluginDir 'languages') -Filter '*.mo' -ErrorAction SilentlyContinue)
  84. if ($moFiles.Count -eq 0) {
  85. Write-Warning "No compiled .mo in languages/. The admin UI renders in English until you compile it (Poedit / msgfmt)."
  86. }
  87. # --- Prepare output ---------------------------------------------------------
  88. if (-not (Test-Path -LiteralPath $OutputDir)) {
  89. New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
  90. Write-Note "Created output directory: $OutputDir"
  91. }
  92. if ($Type -eq 'Plugin') {
  93. $zipName = "$Slug-$version.plugin.zip"
  94. } else {
  95. $zipName = "$Slug-$version.zip"
  96. }
  97. $zipPath = Join-Path $OutputDir $zipName
  98. # --- Stage the files --------------------------------------------------------
  99. $staging = Join-Path ([System.IO.Path]::GetTempPath()) ("studiou-wcmq-build-" + [guid]::NewGuid().ToString('N'))
  100. New-Item -ItemType Directory -Path $staging | Out-Null
  101. try {
  102. $pluginStage = Join-Path $staging $Slug
  103. # Directories excluded from the copy. `loader` is excluded for MuPlugin
  104. # because its stub is hoisted to the archive root; it is kept inside the
  105. # folder for a conventional Plugin package.
  106. $excludeDirs = @('docs', '.git', '.github', '.claude', '.vs', 'node_modules')
  107. if ($Type -eq 'MuPlugin') { $excludeDirs += 'loader' }
  108. # Files excluded by name / pattern.
  109. $excludeFiles = @('*.zip', '*.ps1', '.gitignore', '.gitattributes', 'CLAUDE.md',
  110. 'Thumbs.db', 'desktop.ini', '.DS_Store', $ScriptName)
  111. Write-Step "Staging files (excluding dev artefacts)..."
  112. # robocopy prunes excluded directories rather than copying then deleting, so
  113. # the huge vendored WooCommerce tree under docs/ is never even traversed.
  114. $roboArgs = @($PluginDir, $pluginStage, '/E',
  115. '/XD') + $excludeDirs + @('/XF') + $excludeFiles +
  116. @('/R:1', '/W:1', '/NFL', '/NDL', '/NJH', '/NJS', '/NP', '/NC', '/NS')
  117. robocopy @roboArgs | Out-Null
  118. # robocopy exit codes 0-7 are all success; 8+ is a real failure.
  119. if ($LASTEXITCODE -ge 8) {
  120. throw "robocopy failed with exit code $LASTEXITCODE while staging files."
  121. }
  122. $global:LASTEXITCODE = 0
  123. if (-not (Test-Path -LiteralPath $MainFile.Replace($PluginDir, $pluginStage))) {
  124. throw "Staging did not produce the main plugin file - aborting."
  125. }
  126. # For a mu-plugin, place the loader stub at the archive root so extraction
  127. # into wp-content/mu-plugins/ needs no further steps.
  128. if ($Type -eq 'MuPlugin') {
  129. Copy-Item -LiteralPath $LoaderFile -Destination (Join-Path $staging "$Slug.php")
  130. Write-Note "Loader stub placed at archive root."
  131. }
  132. # --- Compress -----------------------------------------------------------
  133. if (Test-Path -LiteralPath $zipPath) {
  134. Remove-Item -LiteralPath $zipPath -Force
  135. Write-Note "Replaced existing $zipName"
  136. }
  137. Write-Step "Compressing..."
  138. # NOT Compress-Archive: in Windows PowerShell 5.1 it writes entry paths with
  139. # backslashes, which violates the ZIP spec (APPNOTE mandates forward slashes).
  140. # On the Linux host that runs WordPress those extract as flat files with
  141. # literal backslashes in their names, so the plugin never installs. Build the
  142. # archive by hand and force forward-slash entry names.
  143. Add-Type -AssemblyName System.IO.Compression
  144. Add-Type -AssemblyName System.IO.Compression.FileSystem
  145. $stagingRoot = $staging.TrimEnd('\')
  146. $fs = [System.IO.File]::Open($zipPath, [System.IO.FileMode]::Create)
  147. $archive = New-Object System.IO.Compression.ZipArchive($fs, [System.IO.Compression.ZipArchiveMode]::Create)
  148. try {
  149. Get-ChildItem -LiteralPath $staging -Recurse -File | ForEach-Object {
  150. $rel = $_.FullName.Substring($stagingRoot.Length + 1).Replace('\', '/')
  151. [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile(
  152. $archive, $_.FullName, $rel,
  153. [System.IO.Compression.CompressionLevel]::Optimal) | Out-Null
  154. }
  155. }
  156. finally {
  157. $archive.Dispose()
  158. $fs.Dispose()
  159. }
  160. }
  161. finally {
  162. if (Test-Path -LiteralPath $staging) {
  163. Remove-Item -LiteralPath $staging -Recurse -Force -ErrorAction SilentlyContinue
  164. }
  165. }
  166. # --- Verify the archive -----------------------------------------------------
  167. Write-Step "Verifying..."
  168. Add-Type -AssemblyName System.IO.Compression.FileSystem
  169. $zip = [System.IO.Compression.ZipFile]::OpenRead($zipPath)
  170. try {
  171. $entries = $zip.Entries
  172. $fileCount = ($entries | Where-Object { $_.Name -ne '' }).Count
  173. # Guard against dev artefacts leaking in.
  174. $leaked = $entries | Where-Object {
  175. $_.FullName -match '(^|/)docs/' -or
  176. $_.FullName -match '(^|/)\.git/' -or
  177. $_.FullName -match '(^|/)woocommerce/' -or
  178. $_.FullName -like '*CLAUDE.md' -or
  179. $_.FullName -like '*.ps1'
  180. }
  181. if ($leaked) {
  182. $names = ($leaked | Select-Object -First 5 | ForEach-Object { $_.FullName }) -join ', '
  183. throw "Archive contains files that should have been excluded: $names"
  184. }
  185. # Confirm the entry point exists where install expects it.
  186. if ($Type -eq 'MuPlugin') {
  187. $needMain = $entries | Where-Object { $_.FullName -eq "$Slug/$Slug.php" }
  188. $needLoader = $entries | Where-Object { $_.FullName -eq "$Slug.php" }
  189. if (-not $needMain) { throw "Archive is missing $Slug/$Slug.php" }
  190. if (-not $needLoader) { throw "Archive is missing the root loader stub $Slug.php" }
  191. } else {
  192. $needMain = $entries | Where-Object { $_.FullName -eq "$Slug/$Slug.php" }
  193. if (-not $needMain) { throw "Archive is missing $Slug/$Slug.php" }
  194. }
  195. }
  196. finally {
  197. $zip.Dispose()
  198. }
  199. # --- Summary ----------------------------------------------------------------
  200. $sizeKB = [math]::Round((Get-Item -LiteralPath $zipPath).Length / 1KB, 1)
  201. Write-Host ""
  202. Write-Ok "Built $zipName"
  203. Write-Note "Path: $zipPath"
  204. Write-Note "Size: $sizeKB KB"
  205. Write-Note "Files: $fileCount"
  206. Write-Host ""
  207. if ($Type -eq 'MuPlugin') {
  208. Write-Host "Install (mu-plugin):" -ForegroundColor White
  209. Write-Note "Extract the zip directly into wp-content/mu-plugins/ so you get:"
  210. Write-Note " wp-content/mu-plugins/$Slug.php (loader stub)"
  211. Write-Note " wp-content/mu-plugins/$Slug/ (plugin)"
  212. Write-Note "Then: WooCommerce -> Mail Queue -> Enable. Ships disabled."
  213. } else {
  214. Write-Host "Install (regular plugin):" -ForegroundColor White
  215. Write-Note "wp-admin -> Plugins -> Add New -> Upload Plugin -> choose this zip, then Activate."
  216. Write-Note "Then: WooCommerce -> Mail Queue -> Enable. Ships disabled."
  217. }
  218. Write-Host ""