# ESBuild Setup Guide ## Overview Migrate to a single bundled `main.js` using ESBuild for better performance, simplified deployment, and broader browser compatibility (ES2020 target = Chrome 80+, Firefox 72+, Safari 13.1+, Edge 80+). **Target Bundle Size**: ~100KB minified + gzip **Browser Support**: ES2020 (Chrome 80+, Firefox 72+, Safari 13.1+, Edge 80+, Ubuntu 22.04 LTS browsers) **Architecture**: SSR-first, procedural TypeScript, progressive enhancement (per PROJECT_GUIDELINES.md) --- ## Status: What's Already Done ✅ ### 1. package.json Dependencies (Already Correct) **File**: `package.json` **Status**: ✅ Already configured - NO CHANGES NEEDED Dependencies (lines 14-26): - `htmx.org`, `alpinejs`, `lunr`, `highlight.js` already in dependencies - `esbuild`, `typescript`, TailwindCSS tooling already in devDependencies ### 2. Build Scripts (Already Correct) **File**: `package.json` **Status**: ✅ Already configured - NO CHANGES NEEDED Lines 8-10: ```json "build:ts": "esbuild web/src/main.ts --bundle --outfile=web/static/main.js --sourcemap --target=es2020 --minify", "watch:ts": "esbuild web/src/main.ts --bundle --outfile=web/static/main.js --sourcemap --target=es2020", "dev": "concurrently \"npm run watch:css\" \"npm run watch:ts\" \"npm run watch:templ\"", ``` Already using `--target=es2020` for broad browser compatibility. ### 3. Entry Point (Already Exists) **File**: `web/src/main.ts` **Status**: ✅ Already exists - NO CHANGES NEEDED Already imports all 29 modules correctly (24 lines). Main.ts stays simple - imports happen in individual files where used. --- ## Phase 1: Clean Up docs.ts **File**: `web/src/docs.ts` (99 lines) **Status**: Imports already added ✅ **Lines to modify**: 50, 56, 73 (remove window globals), 99 (replace with Alpine) **Note**: Lines 3-4 already have the correct imports: ```typescript import * as lunr from "lunr"; import hljs from "highlight.js"; ``` ### Step 1: Remove (window as any).lunr check **Location**: Line 50 **Current** (lines 49-53): ```typescript if (!(window as any).lunr) { console.warn("Lunr.js not loaded"); return; } ``` **Change to**: ```typescript // Lunr now bundled via ESBuild ``` ### Step 2: Replace (window as any).lunrIndex with direct lunr usage **Location**: Line 56 **Current** (lines 55-62): ```typescript const idx = (window as any).lunrIndex; if (!idx) { searchResults.innerHTML = '
Search index not loaded
'; searchResults.classList.remove("hidden"); return; } ``` **Change to**: ```typescript const idx = lunr.Builder.loadJs(searchIndex); if (!idx) { searchResults.innerHTML = 'Search index not loaded
'; searchResults.classList.remove("hidden"); return; } ``` ### Step 3: Replace (window as any).docsData with direct import **Location**: Line 73 **Current** (lines 70-76): ```typescript .map((result: { ref: string }) => { const doc = (window as any).docsData?.[result.ref]; if (!doc) return ""; ``` **Change to**: ```typescript .map((result: { ref: string }) => { const doc = docs[result.ref]; if (!doc) return ""; ``` ### Step 4: Replace window export with Alpine global **Location**: Line 99 **Current** (lines 98-99): ```typescript document.addEventListener("DOMContentLoaded", () => { initializeDocsSearch(); }); (window as any).toggleSidebar = toggleSidebar; ``` **Change to**: ```typescript document.addEventListener("DOMContentLoaded", () => { initializeDocsSearch(); // Register with Alpine globally if (typeof window.Alpine !== 'undefined') { window.Alpine.effect(() => { window.Alpine.global('docs', { toggleSidebar }); }); } }); ``` **Why Alpine.global()**: Makes `toggleSidebar()` available to Alpine templates via `@click="docs.toggleSidebar()"` --- ## Phase 2: Migrate TypeScript Files to Alpine Registration **Approach**: Replace `(window as any)` exports with Alpine.js global registration ### Architecture Note: Alpine.js for Client-Side State **Why Alpine over window exports**: - Modern, reactive framework (already in package.json) - Clean template syntax: `@click` instead of `onclick="window.func()"` - Built-in state management: `x-data`, `x-show`, `x-model` - Works with SSR (progressive enhancement) - No global namespace pollution **Hybrid approach**: - **Alpine**: Client-side state (modals, dropdowns, theme, forms) - **HTMX**: Server calls (already using for form submissions) ### Step 1: Create Alpine Registration Helper **New file**: `web/src/alpine.ts` ```typescript import Alpine from 'alpinejs'; // Initialize Alpine window.Alpine = Alpine; Alpine.start(); // Re-export Alpine for other modules to use export { Alpine }; ``` **Add to main.ts**: Append this line at the end of `web/src/main.ts`: ```typescript import './alpine'; ``` ### Step 2: Update TypeScript Files to Register with Alpine #### Pattern: Object Registration (Multiple Related Functions) **Example**: `web/src/toast.ts` **Current** (lines 229-236): ```typescript (window as any).showToast = { error: (message: string, duration?: number) => showToast(message, "error", duration), success: (message: string, duration?: number) => showToast(message, "success", duration), info: (message: string, duration?: number) => showToast(message, "info", duration), }; ``` **Change to**: ```typescript import { Alpine } from './alpine'; // Register toast API with Alpine Alpine.global('showToast', { error: (message: string, duration?: number) => showToast(message, "error", duration), success: (message: string, duration?: number) => showToast(message, "success", duration), info: (message: string, duration?: number) => showToast(message, "info", duration), }); ``` **Usage in templates**: ```html ``` #### Pattern: Namespace Registration (Grouping Related Functions) **Example**: `web/src/api.ts` **Current** (lines 90-99): ```typescript (window as any).api = { get: apiGet, post: apiPost, put: apiPut, delete: apiDelete, patch: apiPatch, handleResponse, handleVoidResponse, handleError, }; ``` **Change to**: ```typescript import { Alpine } from './alpine'; Alpine.global('api', { get: apiGet, post: apiPost, put: apiPut, delete: apiDelete, patch: apiPatch, handleResponse, handleVoidResponse, handleError, }); ``` **Usage in templates**: ```html ``` #### Pattern: Stateful Components (Dropdowns, Modals) **Example**: `web/src/header.ts` (theme dropdown) **Current approach**: Multiple window exports **New approach**: Create Alpine component with state **Add to `web/src/header.ts`**: ```typescript import { Alpine } from './alpine'; // Register theme dropdown component Alpine.data('themeDropdown', () => ({ open: false, toggle() { this.open = !this.open; }, changeTheme(theme: string) { changeThemeTo(theme); // Reuse existing function this.open = false; }, init() { // Load saved theme on init applyTheme(loadTheme()); } })); ``` **Usage in templates**: ```html