From 1f075a932b814bccc6b6cefdcfd62a76cc73d471 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 18 Mar 2026 05:14:37 +0000 Subject: [PATCH 1/5] Cache frontend build across setup runs Skip the frontend npm install + build if frontend/dist already exists. Previously setup.ps1 nuked node_modules and package-lock.json on every run, and both scripts always rebuilt even when dist/ was already present. On a git clone editable install, the first setup run still builds the frontend as before. Subsequent runs skip it, saving several minutes. To force a rebuild, delete frontend/dist and re-run setup. --- studio/setup.ps1 | 6 +++--- studio/setup.sh | 11 ++++------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/studio/setup.ps1 b/studio/setup.ps1 index 2420448deb..9f152aa477 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -748,8 +748,11 @@ Write-Host "" # ========================================================================== # PHASE 2: Frontend build (skip if pip-installed -- already bundled) # ========================================================================== +$DistDir = Join-Path $FrontendDir "dist" if ($IsPipInstall) { Write-Host "[OK] Running from pip install - frontend already bundled, skipping build" -ForegroundColor Green +} elseif (Test-Path $DistDir) { + Write-Host "[OK] Frontend already built (frontend/dist exists). To rebuild, delete frontend/dist and re-run setup." -ForegroundColor Green } else { Write-Host "" Write-Host "Building frontend..." -ForegroundColor Cyan @@ -758,9 +761,6 @@ if ($IsPipInstall) { $prevEAP_npm = $ErrorActionPreference $ErrorActionPreference = "Continue" Push-Location $FrontendDir - # Remove stale node_modules and package-lock.json to avoid version conflicts - if (Test-Path "node_modules") { Remove-Item -Recurse -Force "node_modules" } - if (Test-Path "package-lock.json") { Remove-Item -Force "package-lock.json" } npm install 2>&1 | Out-Null if ($LASTEXITCODE -ne 0) { Pop-Location diff --git a/studio/setup.sh b/studio/setup.sh index 4c8a6c7dde..7070b8a089 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -41,13 +41,10 @@ if [[ "$keynames" == *$'\nCOLAB_'* ]]; then fi # ── Detect whether frontend needs building ── -# Only skip when BOTH conditions are true: -# 1. We're inside site-packages (PyPI / pip install, not editable) -# 2. dist/ already exists (pre-built in the wheel) -# Otherwise always (re)build — handles upgrades, editable installs, and -# pip-from-source where dist/ was never built. -if [[ "$SCRIPT_DIR" == */site-packages/* ]] && [ -d "$SCRIPT_DIR/frontend/dist" ]; then - echo "✅ Frontend pre-built (PyPI) — skipping Node/npm check." +# Skip frontend build if dist/ already exists (PyPI wheel, previous build, etc.) +# To force a rebuild, delete frontend/dist and re-run setup. +if [ -d "$SCRIPT_DIR/frontend/dist" ]; then + echo "✅ Frontend already built (frontend/dist exists) -- skipping Node/npm check." else NEED_NODE=true if command -v node &>/dev/null && command -v npm &>/dev/null; then From 5e36f09664448694a519397cc176a9958ad2a677 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 18 Mar 2026 05:44:16 +0000 Subject: [PATCH 2/5] Rebuild frontend when source files are newer than dist/ Instead of only checking if dist/ exists, compare source file timestamps against the dist/ directory. If any file in frontend/src/ is newer than dist/, trigger a rebuild. This handles the case where a developer pulls new frontend changes and re-runs setup -- stale assets get rebuilt automatically. --- studio/setup.ps1 | 17 +++++++++++++++-- studio/setup.sh | 15 ++++++++++++--- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/studio/setup.ps1 b/studio/setup.ps1 index 9f152aa477..07eaf0ce92 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -749,11 +749,24 @@ Write-Host "" # PHASE 2: Frontend build (skip if pip-installed -- already bundled) # ========================================================================== $DistDir = Join-Path $FrontendDir "dist" +$SrcDir = Join-Path $FrontendDir "src" +# Skip build if dist/ exists and no source file is newer than dist/ +$NeedFrontendBuild = $true if ($IsPipInstall) { + $NeedFrontendBuild = $false Write-Host "[OK] Running from pip install - frontend already bundled, skipping build" -ForegroundColor Green } elseif (Test-Path $DistDir) { - Write-Host "[OK] Frontend already built (frontend/dist exists). To rebuild, delete frontend/dist and re-run setup." -ForegroundColor Green -} else { + $DistTime = (Get-Item $DistDir).LastWriteTime + $NewerSrc = Get-ChildItem -Path $SrcDir -Recurse -File -ErrorAction SilentlyContinue | + Where-Object { $_.LastWriteTime -gt $DistTime } | Select-Object -First 1 + if (-not $NewerSrc) { + $NeedFrontendBuild = $false + Write-Host "[OK] Frontend already built and up to date -- skipping build" -ForegroundColor Green + } else { + Write-Host "[INFO] Frontend source changed since last build -- rebuilding..." -ForegroundColor Yellow + } +} +if ($NeedFrontendBuild -and -not $IsPipInstall) { Write-Host "" Write-Host "Building frontend..." -ForegroundColor Cyan # npm writes warnings to stderr; lower ErrorActionPreference so PS doesn't diff --git a/studio/setup.sh b/studio/setup.sh index 7070b8a089..758dc6676a 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -41,10 +41,19 @@ if [[ "$keynames" == *$'\nCOLAB_'* ]]; then fi # ── Detect whether frontend needs building ── -# Skip frontend build if dist/ already exists (PyPI wheel, previous build, etc.) -# To force a rebuild, delete frontend/dist and re-run setup. +# Skip if dist/ exists AND no source file is newer than dist/. +# This handles: PyPI installs (dist/ bundled), repeat runs (no changes), +# and upgrades/pulls (source newer than dist/ triggers rebuild). +_NEED_FRONTEND_BUILD=true if [ -d "$SCRIPT_DIR/frontend/dist" ]; then - echo "✅ Frontend already built (frontend/dist exists) -- skipping Node/npm check." + # Check if any src/ file is newer than dist/ + _newest_src=$(find "$SCRIPT_DIR/frontend/src" -type f -newer "$SCRIPT_DIR/frontend/dist" 2>/dev/null | head -1) + if [ -z "$_newest_src" ]; then + _NEED_FRONTEND_BUILD=false + fi +fi +if [ "$_NEED_FRONTEND_BUILD" = false ]; then + echo "✅ Frontend already built and up to date -- skipping Node/npm check." else NEED_NODE=true if command -v node &>/dev/null && command -v npm &>/dev/null; then From 153c203a89e5beecb625d147d1e80b41df476519 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 18 Mar 2026 05:59:42 +0000 Subject: [PATCH 3/5] Fix frontend rebuild detection and decouple oxc-validator install Address review feedback: - Check entire frontend/ directory for changes, not just src/. The build also depends on package.json, vite.config.ts, tailwind.config.ts, public/, and other config files. A change to any of these now triggers a rebuild. - Move oxc-validator npm install outside the frontend build gate in setup.sh so it always runs on setup, matching setup.ps1 which already had it outside the gate. --- studio/setup.ps1 | 16 ++++++++++++---- studio/setup.sh | 24 +++++++++++++++++------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/studio/setup.ps1 b/studio/setup.ps1 index 07eaf0ce92..e9a211eb45 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -749,17 +749,25 @@ Write-Host "" # PHASE 2: Frontend build (skip if pip-installed -- already bundled) # ========================================================================== $DistDir = Join-Path $FrontendDir "dist" -$SrcDir = Join-Path $FrontendDir "src" -# Skip build if dist/ exists and no source file is newer than dist/ +# Skip build if dist/ exists and no tracked input is newer than dist/. +# Checks src/, public/, package.json, config files -- not just src/. $NeedFrontendBuild = $true if ($IsPipInstall) { $NeedFrontendBuild = $false Write-Host "[OK] Running from pip install - frontend already bundled, skipping build" -ForegroundColor Green } elseif (Test-Path $DistDir) { $DistTime = (Get-Item $DistDir).LastWriteTime - $NewerSrc = Get-ChildItem -Path $SrcDir -Recurse -File -ErrorAction SilentlyContinue | + # Check src/ and public/ recursively + $NewerFile = Get-ChildItem -Path $FrontendDir -Include "src","public" -Directory -ErrorAction SilentlyContinue | + ForEach-Object { Get-ChildItem -Path $_.FullName -Recurse -File -ErrorAction SilentlyContinue } | Where-Object { $_.LastWriteTime -gt $DistTime } | Select-Object -First 1 - if (-not $NewerSrc) { + # Also check top-level config files (package.json, vite.config.ts, etc.) + if (-not $NewerFile) { + $NewerFile = Get-ChildItem -Path $FrontendDir -File -ErrorAction SilentlyContinue | + Where-Object { $_.Name -match '\.(json|ts|js|mjs)$' -and $_.LastWriteTime -gt $DistTime } | + Select-Object -First 1 + } + if (-not $NewerFile) { $NeedFrontendBuild = $false Write-Host "[OK] Frontend already built and up to date -- skipping build" -ForegroundColor Green } else { diff --git a/studio/setup.sh b/studio/setup.sh index 758dc6676a..2c93d57f78 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -41,14 +41,19 @@ if [[ "$keynames" == *$'\nCOLAB_'* ]]; then fi # ── Detect whether frontend needs building ── -# Skip if dist/ exists AND no source file is newer than dist/. +# Skip if dist/ exists AND no tracked input is newer than dist/. +# Checks src/, public/, package.json, config files -- not just src/. # This handles: PyPI installs (dist/ bundled), repeat runs (no changes), # and upgrades/pulls (source newer than dist/ triggers rebuild). _NEED_FRONTEND_BUILD=true if [ -d "$SCRIPT_DIR/frontend/dist" ]; then - # Check if any src/ file is newer than dist/ - _newest_src=$(find "$SCRIPT_DIR/frontend/src" -type f -newer "$SCRIPT_DIR/frontend/dist" 2>/dev/null | head -1) - if [ -z "$_newest_src" ]; then + _changed=$(find "$SCRIPT_DIR/frontend" -maxdepth 1 -name "*.json" -o -name "*.ts" -o -name "*.js" -o -name "*.mjs" | \ + xargs -r find -newer "$SCRIPT_DIR/frontend/dist" 2>/dev/null | head -1) + if [ -z "$_changed" ]; then + _changed=$(find "$SCRIPT_DIR/frontend/src" "$SCRIPT_DIR/frontend/public" \ + -type f -newer "$SCRIPT_DIR/frontend/dist" 2>/dev/null | head -1) + fi + if [ -z "$_changed" ]; then _NEED_FRONTEND_BUILD=false fi fi @@ -152,12 +157,17 @@ run_quiet "npm run build" npm run build _restore_gitignores trap - EXIT -cd "$SCRIPT_DIR/backend/core/data_recipe/oxc-validator" -run_quiet "npm install (oxc validator runtime)" npm install cd "$SCRIPT_DIR" echo "✅ Frontend built to frontend/dist" -fi # end frontend dist check +fi # end frontend build check + +# ── oxc-validator runtime (always install, independent of frontend build) ── +if [ -d "$SCRIPT_DIR/backend/core/data_recipe/oxc-validator" ]; then + cd "$SCRIPT_DIR/backend/core/data_recipe/oxc-validator" + run_quiet "npm install (oxc validator runtime)" npm install + cd "$SCRIPT_DIR" +fi # ── 6. Python venv + deps ── From 21de1c5399ab7367e56505ccabe1e68d766c0a56 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 18 Mar 2026 07:20:04 +0000 Subject: [PATCH 4/5] Fix broken frontend freshness detection in setup scripts - setup.sh: Replace broken `find | xargs find -newer` pipeline with single `find ... -newer` call. The old pipeline produced "paths must precede expression" errors (silently suppressed by 2>/dev/null), causing top-level config changes to never trigger a rebuild. - setup.sh: Add `command -v npm` guard to oxc-validator block so it does not fail when Node was not installed (build-skip path). - setup.ps1: Replace `Get-ChildItem -Include` (unreliable without -Recurse on PS 5.1) with explicit directory paths for src/ and public/ scanning. - Both: Add *.html to tracked file patterns so index.html (Vite entry point) changes trigger a rebuild. - Both: Use -print -quit instead of piping to head -1 for efficiency. --- studio/setup.ps1 | 18 ++++++++++++------ studio/setup.sh | 13 ++++++++----- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/studio/setup.ps1 b/studio/setup.ps1 index e9a211eb45..1046304c46 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -757,14 +757,20 @@ if ($IsPipInstall) { Write-Host "[OK] Running from pip install - frontend already bundled, skipping build" -ForegroundColor Green } elseif (Test-Path $DistDir) { $DistTime = (Get-Item $DistDir).LastWriteTime - # Check src/ and public/ recursively - $NewerFile = Get-ChildItem -Path $FrontendDir -Include "src","public" -Directory -ErrorAction SilentlyContinue | - ForEach-Object { Get-ChildItem -Path $_.FullName -Recurse -File -ErrorAction SilentlyContinue } | - Where-Object { $_.LastWriteTime -gt $DistTime } | Select-Object -First 1 - # Also check top-level config files (package.json, vite.config.ts, etc.) + # Check src/ and public/ recursively using explicit paths + $TrackedDirs = @("src", "public") | + ForEach-Object { Join-Path $FrontendDir $_ } | + Where-Object { Test-Path $_ } + $NewerFile = $null + if ($TrackedDirs.Count -gt 0) { + $NewerFile = $TrackedDirs | + ForEach-Object { Get-ChildItem -Path $_ -Recurse -File -ErrorAction SilentlyContinue } | + Where-Object { $_.LastWriteTime -gt $DistTime } | Select-Object -First 1 + } + # Also check top-level config and entry files (package.json, vite.config.ts, index.html, etc.) if (-not $NewerFile) { $NewerFile = Get-ChildItem -Path $FrontendDir -File -ErrorAction SilentlyContinue | - Where-Object { $_.Name -match '\.(json|ts|js|mjs)$' -and $_.LastWriteTime -gt $DistTime } | + Where-Object { $_.Name -match '\.(json|ts|js|mjs|html)$' -and $_.LastWriteTime -gt $DistTime } | Select-Object -First 1 } if (-not $NewerFile) { diff --git a/studio/setup.sh b/studio/setup.sh index 2c93d57f78..98d6d4b8f4 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -42,16 +42,19 @@ fi # ── Detect whether frontend needs building ── # Skip if dist/ exists AND no tracked input is newer than dist/. -# Checks src/, public/, package.json, config files -- not just src/. +# Checks top-level config/entry files and src/, public/ recursively. # This handles: PyPI installs (dist/ bundled), repeat runs (no changes), # and upgrades/pulls (source newer than dist/ triggers rebuild). _NEED_FRONTEND_BUILD=true if [ -d "$SCRIPT_DIR/frontend/dist" ]; then - _changed=$(find "$SCRIPT_DIR/frontend" -maxdepth 1 -name "*.json" -o -name "*.ts" -o -name "*.js" -o -name "*.mjs" | \ - xargs -r find -newer "$SCRIPT_DIR/frontend/dist" 2>/dev/null | head -1) + # Check top-level config and entry files (package.json, vite.config.ts, index.html, etc.) + _changed=$(find "$SCRIPT_DIR/frontend" -maxdepth 1 \ + \( -name "*.json" -o -name "*.ts" -o -name "*.js" -o -name "*.mjs" -o -name "*.html" \) \ + -newer "$SCRIPT_DIR/frontend/dist" -print -quit 2>/dev/null) + # Check src/ and public/ recursively if [ -z "$_changed" ]; then _changed=$(find "$SCRIPT_DIR/frontend/src" "$SCRIPT_DIR/frontend/public" \ - -type f -newer "$SCRIPT_DIR/frontend/dist" 2>/dev/null | head -1) + -type f -newer "$SCRIPT_DIR/frontend/dist" -print -quit 2>/dev/null) fi if [ -z "$_changed" ]; then _NEED_FRONTEND_BUILD=false @@ -163,7 +166,7 @@ echo "✅ Frontend built to frontend/dist" fi # end frontend build check # ── oxc-validator runtime (always install, independent of frontend build) ── -if [ -d "$SCRIPT_DIR/backend/core/data_recipe/oxc-validator" ]; then +if [ -d "$SCRIPT_DIR/backend/core/data_recipe/oxc-validator" ] && command -v npm &>/dev/null; then cd "$SCRIPT_DIR/backend/core/data_recipe/oxc-validator" run_quiet "npm install (oxc validator runtime)" npm install cd "$SCRIPT_DIR" From 3a9be0410819616a3bc6918e22d1f0ada3bdec81 Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Mon, 6 Jul 2026 10:32:33 +0000 Subject: [PATCH 5/5] Guard the frontend freshness find against missing src/public dirs Under set -euo pipefail, find over a missing directory exits non-zero, and the command substitution propagates that so setup aborts on a slim dist-only layout (a PyPI wheel that ships frontend/dist without src/ or public/). Pass only the directories that exist to find, so the cache-hit path completes instead of aborting. --- studio/setup.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/studio/setup.sh b/studio/setup.sh index 98d6d4b8f4..c5055d6d02 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -51,10 +51,16 @@ if [ -d "$SCRIPT_DIR/frontend/dist" ]; then _changed=$(find "$SCRIPT_DIR/frontend" -maxdepth 1 \ \( -name "*.json" -o -name "*.ts" -o -name "*.js" -o -name "*.mjs" -o -name "*.html" \) \ -newer "$SCRIPT_DIR/frontend/dist" -print -quit 2>/dev/null) - # Check src/ and public/ recursively + # Check src/ and public/ recursively (only dirs that exist; a slimmed + # layout that ships dist/ without src/ or public/ must not abort under set -e) if [ -z "$_changed" ]; then - _changed=$(find "$SCRIPT_DIR/frontend/src" "$SCRIPT_DIR/frontend/public" \ - -type f -newer "$SCRIPT_DIR/frontend/dist" -print -quit 2>/dev/null) + _src_dirs=() + [ -d "$SCRIPT_DIR/frontend/src" ] && _src_dirs+=("$SCRIPT_DIR/frontend/src") + [ -d "$SCRIPT_DIR/frontend/public" ] && _src_dirs+=("$SCRIPT_DIR/frontend/public") + if [ "${#_src_dirs[@]}" -gt 0 ]; then + _changed=$(find "${_src_dirs[@]}" \ + -type f -newer "$SCRIPT_DIR/frontend/dist" -print -quit 2>/dev/null) + fi fi if [ -z "$_changed" ]; then _NEED_FRONTEND_BUILD=false