param( [ValidateSet("web", "tauri")] [string]$Target = "web", [ValidateSet("Release", "Debug")] [string]$Configuration = "Release", [ValidateSet("none", "nsis", "msi")] [string]$TauriBundles = "none", [switch]$InstallDeps, [switch]$SkipInstall, [switch]$DryRun ) $commonScript = Join-Path $PSScriptRoot "script-common.ps1" if (-not (Test-Path $commonScript)) { throw "Missing helper script: $commonScript" } . $commonScript $repoRoot = Resolve-JournalRepoRoot -StartPath $PSScriptRoot $appRoot = Resolve-JournalAppRoot -RepoRoot $repoRoot Clear-JournalProxyEnv # Keep npm cache and temp local to the repo. $npmCacheDir = Join-Path $repoRoot ".npm\cache" $npmTempDir = Join-Path $repoRoot ".tmp\npm-temp" New-Item -ItemType Directory -Force -Path $npmCacheDir, $npmTempDir | Out-Null $env:npm_config_cache = $npmCacheDir $env:npm_config_update_notifier = "false" $env:npm_config_fund = "false" $env:npm_config_audit = "false" $env:npm_config_offline = "false" $env:npm_config_prefer_offline = "false" $env:npm_config_prefer_online = "true" $env:TEMP = $npmTempDir $env:TMP = $npmTempDir if (-not (Get-Command npm -ErrorAction SilentlyContinue)) { throw "npm is required but was not found in PATH." } $nodeModulesPath = Join-Path $appRoot "node_modules" $packageJsonPath = Join-Path $appRoot "package.json" $packageLockPath = Join-Path $appRoot "package-lock.json" $depsHashPath = Join-Path $appRoot "node_modules\.journal-deps.sha256" function Get-JournalNodeDepsHash { param( [Parameter(Mandatory = $true)] [string[]]$Paths ) $hashLines = foreach ($path in $Paths) { if (-not (Test-Path $path)) { continue } (Get-FileHash -Algorithm SHA256 -Path $path).Hash } return ($hashLines -join "`n").Trim() } $hashInputs = @() if (Test-Path $packageJsonPath) { $hashInputs += $packageJsonPath } if (Test-Path $packageLockPath) { $hashInputs += $packageLockPath } if ($hashInputs.Count -eq 0) { throw "package.json not found under $appRoot." } $expectedDepsHash = Get-JournalNodeDepsHash -Paths $hashInputs $shouldInstall = $InstallDeps -or (-not (Test-Path $nodeModulesPath)) $installReason = $null if (-not $shouldInstall -and -not $SkipInstall) { if (-not (Test-Path $depsHashPath)) { $shouldInstall = $true $installReason = "dependency hash missing" } else { $currentDepsHash = (Get-Content $depsHashPath -Raw).Trim() if ($currentDepsHash -ne $expectedDepsHash) { $shouldInstall = $true $installReason = "package.json/lockfile changed" } } } if ($SkipInstall) { $shouldInstall = $false if ($installReason) { Write-Host "SkipInstall set; dependencies may be stale ($installReason)." -ForegroundColor Yellow } } Write-Host "Building Journal.App target '$Target' ($Configuration)..." -ForegroundColor Cyan Write-Host "Using app root: $appRoot" -ForegroundColor DarkGray Push-Location $appRoot try { if ($shouldInstall) { $installArgs = if (Test-Path $packageLockPath) { @("ci", "--no-audit", "--fund=false") } else { @("install", "--no-audit", "--fund=false") } if ($installReason) { Write-Host "Dependencies changed ($installReason). Installing..." -ForegroundColor Yellow } Write-Host "> npm $($installArgs -join ' ')" -ForegroundColor DarkGray if (-not $DryRun) { & npm @installArgs if ($LASTEXITCODE -ne 0) { throw "Dependency install failed with exit code $LASTEXITCODE." } $depsDir = Split-Path $depsHashPath -Parent if (-not (Test-Path $depsDir)) { New-Item -ItemType Directory -Force -Path $depsDir | Out-Null } $expectedDepsHash | Set-Content -Path $depsHashPath -NoNewline } } else { Write-Host "Skipping dependency install (node_modules present and deps unchanged)." -ForegroundColor DarkGray } if ($Target -eq "web") { $buildArgs = @("run", "build") Write-Host "> npm $($buildArgs -join ' ')" -ForegroundColor DarkGray if (-not $DryRun) { & npm @buildArgs if ($LASTEXITCODE -ne 0) { throw "Frontend build failed with exit code $LASTEXITCODE." } } $outputPath = Join-Path $appRoot "build" if ($DryRun) { Write-Host "`nDry run complete (no commands executed)." -ForegroundColor Yellow Write-Host "Expected output: $outputPath" -ForegroundColor Gray } else { Write-Host "`nFrontend build successful." -ForegroundColor Green Write-Host "Output: $outputPath" -ForegroundColor Gray } } else { $tauriArgs = @("run", "tauri", "build") $tauriCliArgs = @() if ($TauriBundles -eq "none") { $tauriCliArgs += "--no-bundle" } else { $tauriCliArgs += @("--bundles", $TauriBundles) } if ($Configuration -eq "Debug") { $tauriCliArgs += "--debug" } if ($tauriCliArgs.Count -gt 0) { $tauriArgs += "--" $tauriArgs += $tauriCliArgs } Write-Host "> npm $($tauriArgs -join ' ')" -ForegroundColor DarkGray if (-not $DryRun) { & npm @tauriArgs if ($LASTEXITCODE -ne 0) { throw "Tauri build failed with exit code $LASTEXITCODE." } } $targetConfigDir = if ($Configuration -eq "Debug") { "debug" } else { "release" } $tauriTargetPath = Join-Path $appRoot "src-tauri\target" $rawExePath = Join-Path $tauriTargetPath "$targetConfigDir\journalapp.exe" if ($DryRun) { Write-Host "`nDry run complete (no commands executed)." -ForegroundColor Yellow if ($TauriBundles -eq "none") { Write-Host "Expected executable: $rawExePath" -ForegroundColor Gray } else { Write-Host "Expected output root: $tauriTargetPath" -ForegroundColor Gray } } else { Write-Host "`nTauri build successful." -ForegroundColor Green if ($TauriBundles -eq "none") { if (Test-Path $rawExePath) { Write-Host "Executable location: $rawExePath" -ForegroundColor Gray } else { $exeCandidates = Get-ChildItem -Path (Join-Path $tauriTargetPath $targetConfigDir) -File -Filter *.exe -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending if ($exeCandidates -and $exeCandidates.Count -gt 0) { Write-Host "Executable location: $($exeCandidates[0].FullName)" -ForegroundColor Gray } else { Write-Host "Output root: $tauriTargetPath" -ForegroundColor Gray } } } else { Write-Host "Output root: $tauriTargetPath" -ForegroundColor Gray } } } } finally { Pop-Location }