diff --git a/start_ui.py b/start_ui.py index 59fd204..b59d579 100644 --- a/start_ui.py +++ b/start_ui.py @@ -141,38 +141,90 @@ def install_npm_deps() -> bool: def build_frontend() -> bool: - """Build the React frontend if dist doesn't exist or is stale.""" + """Build the React frontend if dist doesn't exist or is stale. + + Staleness is determined by comparing modification times of: + - Source files in ui/src/ + - Config files (package.json, vite.config.ts, etc.) + Against the newest file in ui/dist/ + + Includes a 2-second tolerance for FAT32 filesystem compatibility. + """ dist_dir = UI_DIR / "dist" src_dir = UI_DIR / "src" + # FAT32 has 2-second timestamp precision, so we add tolerance to avoid + # false negatives when projects are on USB drives or SD cards + TIMESTAMP_TOLERANCE = 2 + + # Config files that should trigger a rebuild when changed + CONFIG_FILES = [ + "package.json", + "package-lock.json", + "vite.config.ts", + "tailwind.config.ts", + "tsconfig.json", + "tsconfig.node.json", + "postcss.config.js", + "index.html", + ] + # Check if build is needed needs_build = False + trigger_file = None if not dist_dir.exists(): needs_build = True + trigger_file = "dist/ directory missing" elif src_dir.exists(): # Find the newest file in dist/ directory newest_dist_mtime = 0 for dist_file in dist_dir.rglob("*"): - if dist_file.is_file(): - file_mtime = dist_file.stat().st_mtime - if file_mtime > newest_dist_mtime: - newest_dist_mtime = file_mtime + try: + if dist_file.is_file(): + file_mtime = dist_file.stat().st_mtime + if file_mtime > newest_dist_mtime: + newest_dist_mtime = file_mtime + except (FileNotFoundError, PermissionError, OSError): + # File was deleted or became inaccessible during iteration + continue - # Check if any source file is newer than the newest dist file if newest_dist_mtime > 0: - for src_file in src_dir.rglob("*"): - if src_file.is_file() and src_file.stat().st_mtime > newest_dist_mtime: - needs_build = True - break + # Check config files first (these always require rebuild) + for config_name in CONFIG_FILES: + config_path = UI_DIR / config_name + try: + if config_path.exists(): + if config_path.stat().st_mtime > newest_dist_mtime + TIMESTAMP_TOLERANCE: + needs_build = True + trigger_file = config_name + break + except (FileNotFoundError, PermissionError, OSError): + continue + + # Check source files if no config triggered rebuild + if not needs_build: + for src_file in src_dir.rglob("*"): + try: + if src_file.is_file(): + if src_file.stat().st_mtime > newest_dist_mtime + TIMESTAMP_TOLERANCE: + needs_build = True + trigger_file = str(src_file.relative_to(UI_DIR)) + break + except (FileNotFoundError, PermissionError, OSError): + # File was deleted or became inaccessible during iteration + continue else: # No files found in dist, need to rebuild needs_build = True + trigger_file = "dist/ directory is empty" if not needs_build: print(" Frontend already built (up to date)") return True + if trigger_file: + print(f" Rebuild triggered by: {trigger_file}") print(" Building React frontend...") npm_cmd = "npm.cmd" if sys.platform == "win32" else "npm" return run_command([npm_cmd, "run", "build"], cwd=UI_DIR)