# 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
``` ### Files Requiring Alpine Migration **High priority** (used by 10+ templates): 1. `web/src/toast.ts` - Register `showToast` 2. `web/src/api.ts` - Register `api` object 3. `web/src/events.ts` - Register event functions 4. `web/src/storage.ts` - Register storage helpers **Medium priority** (stateful components): 5. `web/src/header.ts` - Create `themeDropdown` component 6. `web/src/woodPaneling.ts` - Create `woodPaneling` component 7. `web/src/collections.ts` - Register collection functions 8. `web/src/conflicts.ts` - Register conflict functions 9. `web/src/search.ts` - Register search functions 10. `web/src/dom.ts` - Register DOM helpers **Lower priority** (page-specific): 11. `web/src/dashboard.ts` - Dashboard-specific functions 12. `web/src/devices.ts` - Device management functions 13. `web/src/queue.ts` - Queue management functions 14. `web/src/analytics.ts` - Analytics functions (Chart.js) 15. `web/src/admin.ts` - Admin functions **Keep as-is for now** (already working): - Lunr/Highlight.js imports (docs.ts) - already handled - Chart.js usage - already loaded via CDN --- ## Phase 3: Update Templates to Alpine Directives ### Template Migration Strategy **151 onclick handlers need migration** across 27 templates. Use this approach: 1. **Add `x-data` component** to sections with state 2. **Replace `onclick`** with `@click` 3. **Replace `class="hidden"`** with `x-show="!open"` 4. **Add transitions** with `x-transition` 5. **Use `x-model`** for form inputs ### Template Changes (27 files) --- #### 1. templates/collections.templ **File**: `templates/collections.templ` (271 lines) **Remove script blocks** (lines 12-13, 111-112): DELETE all individual ` ``` **Change to**: ```templ ``` **Update onclick handlers**: **Line 63** - Collection card: ```templ
``` **Line 119** - Back button: ```templ ``` **Change to Alpine**: ```templ
``` **Update user menu** (lines 57-60, 65): ```templ ``` **Change to**: ```templ
``` --- #### 7. templates/analytics.templ **File**: `templates/analytics.templ` (100 lines) **Remove first script block** (lines 10-14): ```templ ``` DELETE these lines entirely. **Note**: Chart.js remains on CDN (intentionally not bundled - 3.4MB, only used on 1 page). **Replace analytics.js script** (line 97): ```templ ``` **Change to**: ```templ ``` --- #### 8. templates/bookshelf.templ **File**: `templates/bookshelf.templ` (266 lines) **Remove first script block** (lines 10-13): ```templ ``` DELETE these lines entirely. **Replace bookshelf.js script** (line 63): ```templ ``` **Change to**: ```templ ``` --- #### 9. templates/devices.templ **File**: `templates/devices.templ` (870 lines) **Replace script block** (lines 12-16): ```templ ``` **Change to**: ```templ ``` --- #### 10. templates/index.templ **File**: `templates/index.templ` (165 lines) **Remove first script block** (lines 10-12): ```templ ``` DELETE these lines entirely. **Replace index.js script** (line 138): ```templ ``` **Change to**: ```templ ``` --- #### 11. templates/login.templ **File**: `templates/login.templ` (81 lines) **Replace script block** (lines 10-12): ```templ ``` **Change to**: ```templ ``` --- #### 12. templates/profile.templ **File**: `templates/profile.templ` (68 lines) **Replace script block** (lines 9-11): ```templ ``` **Change to**: ```templ ``` --- #### 13. templates/queue.templ **File**: `templates/queue.templ` (169 lines) **Remove first script block** (lines 12-15): ```templ ``` DELETE these lines entirely. **Replace queue.js script** (line 166): ```templ ``` **Change to**: ```templ ``` --- #### 14. templates/conflicts.templ **File**: `templates/conflicts.templ` (239 lines) **Remove first script block** (lines 12-15): ```templ ``` DELETE these lines entirely. **Replace conflicts.js script** (line 234): ```templ ``` **Change to**: ```templ ``` --- #### 15. templates/custom_section.templ **File**: `templates/custom_section.templ` (168 lines) **Replace script block** (lines 10-13): ```templ ``` **Change to**: ```templ ``` --- #### 16. templates/admin_users.templ **File**: `templates/admin_users.templ` (145 lines) **Replace script block** (lines 9-12): ```templ ``` **Change to**: ```templ ``` --- #### 17. templates/collection_rules.templ **File**: `templates/collection_rules.templ` (420 lines) **Replace script block** (lines 10-12): ```templ ``` **Change to**: ```templ ``` --- #### 18. templates/progress.templ **File**: `templates/progress.templ` (114 lines) **Replace script block** (lines 12-14): ```templ ``` **Change to**: ```templ ``` --- #### 19. templates/register.templ **File**: `templates/register.templ` (97 lines) **Replace script block** (lines 10-12): ```templ ``` **Change to**: ```templ ``` --- #### 20. templates/settings.templ **File**: `templates/settings.templ` (157 lines) **Replace script block** (lines 9-11): ```templ ``` **Change to**: ```templ ``` --- #### 21. templates/stats.templ **File**: `templates/stats.templ` (162 lines) **Replace script block** (lines 9-11): ```templ ``` **Change to**: ```templ ``` --- #### 22. templates/sync.templ **File**: `templates/sync.templ` (184 lines) **Replace script block** (lines 9-12): ```templ ``` **Change to**: ```templ ``` --- #### 23. templates/testing_templ.templ **File**: `templates/testing_templ.templ` (115 lines) **Replace script block** (lines 9-11): ```templ ``` **Change to**: ```templ ``` --- #### 24. themes/obsidian.templ **File**: `themes/obsidian.templ` (91 lines) **Replace script block** (lines 9-11): ```templ ``` **Change to**: ```templ ``` --- #### 25. themes/whatsapp.templ **File**: `themes/whatsapp.templ` (86 lines) **Replace script block** (lines 9-11): ```templ ``` **Change to**: ```templ ``` --- #### 26. themes/midnight.templ **File**: `themes/midnight.templ` (91 lines) **Replace script block** (lines 9-11): ```templ ``` **Change to**: ```templ ``` --- #### 27. themes/sunset.templ (same pattern as other themes) **File**: `themes/sunset.templ` (86 lines) **Replace script block** (lines 9-11): ```templ ``` **Change to**: ```templ ``` --- ### Remaining Templates (7-26) For templates **7-26**, follow this pattern: 1. **Remove all individual script tags** (usually 2-5 scripts) 2. **Replace with single script**: `` 3. **Replace `onclick` with `@click`** 4. **Add `x-data` for stateful components** (modals, dropdowns) **Quick find-replace patterns**: ```bash # In each template file: onclick="funcName()" → @click="module.funcName()" onclick="func('param')" → @click="module.func('param')" class="hidden" → x-show="!isOpen" (with x-data parent) ``` **Common stateful patterns**: **Modal pattern**: ```templ
``` **Confirm delete pattern**: ```templ ``` **Form submission** (keep HTMX for server calls): ```templ
Name required
``` **File**: `themes/sunset.templ` (86 lines) **Replace script block** (lines 9-11): ```templ ``` **Change to**: ```templ ``` --- ## Phase 4: Build and Verify ### Step 1: Create Alpine Registration File **File**: `web/src/alpine.ts` (already exists, needs update) **Current file** (missing window.Alpine): ```typescript import Alpine from "alpinejs"; // Initialize Alpine Alpine.start(); // Re-export Alpine for other modules to use export { Alpine }; ``` **Change to** (add window.Alpine + type declaration): ```typescript import Alpine from "alpinejs"; // Extend Window interface to include Alpine declare global { interface Window { Alpine: typeof Alpine; } } // Initialize Alpine window.Alpine = Alpine; Alpine.start(); // Re-export Alpine for other modules to use export { Alpine }; ``` **Why type declaration**: Fixes TypeScript error "Property 'Alpine' does not exist on type 'Window'" **Why `window.Alpine`**: Required for Alpine DevTools browser extension to work. ### Step 2: Update main.ts **File**: `web/src/main.ts` **Add at end** (after all other imports): ```typescript import './alpine'; ``` ### Step 3: Build the Bundle ```bash npm run build:ts ``` **Expected output**: Creates `web/static/main.js` (~350-450KB unminified, ~120-150KB minified with Alpine bundled) + `main.js.map` sourcemap **Why larger**: Alpine.js adds ~15KB gzipped to bundle (worth it for cleaner code). ### Step 4: Regenerate Templates ```bash templ generate ``` **Expected output**: Regenerates all .templ files with updated Alpine directives ### Step 5: Verify Alpine is Loaded ```bash head -n 50 web/static/main.js | grep -i alpine ``` **Check for**: Alpine initialization code present in bundle. ### Step 6: Test in Browser 1. Start dev server: `go run .` 2. Open browser DevTools (F12) → Console 3. Verify Alpine loaded: ```javascript typeof window.Alpine !== 'undefined' // should be true ``` 4. Test key pages: - **Dashboard** (tests theme dropdown, wood paneling) - **Docs page** (tests lunr search, sidebar toggle) - **Collections** (tests modal, multiple onclick handlers) - **Header** (tests theme dropdown, user menu) - **Devices** (tests multiple modals) 5. **Check Alpine DevTools** (optional): - Install Alpine DevTools browser extension - Inspect `x-data` components in DevTools panel - Verify reactive state changes ### Step 7: Verify Functionality **In browser console, test Alpine globals**: ```javascript // Should all be accessible via Alpine Alpine.stores?.toast // Toast store Alpine.global('showToast') // Global function ``` **Test interactions**: - Theme dropdown opens/closes smoothly - Modals open with transitions - Toast notifications appear - Forms still submit via HTMX - Search works on docs page - All 151 onclick handlers work with `@click` ### Step 8: Check Bundle Size ```bash ls -lh web/static/main.js ``` **Expected**: ~120-150KB (minified with Alpine included) **Breakdown**: - App code: ~100KB - Alpine.js: ~15KB gzipped - Lunr: ~10KB gzipped - Highlight.js: ~5KB gzipped - **Total**: ~130KB / ~40KB gzipped --- ## Phase 5: Migrate TypeScript Files to Alpine (Step-by-Step) ### Priority 1: Core Utilities (Do These First) #### 1. web/src/toast.ts **Current state**: Already has Alpine import at line 1, but not using it yet **Add import** (already at line 1): ```typescript import { Alpine } from "./alpine"; ``` **Replace export** (lines 229-236): ```typescript // Before (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 // After - 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), }); ``` **Template usage**: ```html ``` #### 2. web/src/api.ts **Add import** (at top): ```typescript import { Alpine } from './alpine'; ``` **Replace export** (lines 90-99): ```typescript // Before (window as any).api = { get: apiGet, post: apiPost, put: apiPut, delete: apiDelete, patch: apiPatch, handleResponse, handleVoidResponse, handleError, }; ``` **Change to**: ```typescript // After - Register API with Alpine Alpine.global('api', { get: apiGet, post: apiPost, put: apiPut, delete: apiDelete, patch: apiPatch, handleResponse, handleVoidResponse, handleError, }); ``` **Also update line 85-87** (toast error calls): ```typescript // Before if ((window as any).showToast?.error) { (window as any).showToast.error(message); } // After (will work once toast.ts is migrated) if (window.Alpine?.stores?.toast?.error) { window.Alpine.stores.toast.error(message); } // Or keep using window.showToast during migration ``` **Template usage**: ```html ``` #### 3. web/src/storage.ts **Add import** (at top): ```typescript import { Alpine } from './alpine'; ``` **Replace export** (lines 53-67): ```typescript // Before (window as any).storage = { getToken, setToken, removeToken, getRefreshToken, setRefreshToken, removeRefreshToken, getTheme, setTheme, getSelectedLibrary, setSelectedLibrary, getSelectedBook, setSelectedBook, clearAll, }; ``` **Change to**: ```typescript // After - Register storage helpers with Alpine Alpine.global('storage', { getToken, setToken, removeToken, getRefreshToken, setRefreshToken, removeRefreshToken, getTheme, setTheme, getSelectedLibrary, setSelectedLibrary, getSelectedBook, setSelectedBook, clearAll, }); ``` **Template usage**: ```html ``` ### Priority 2: Stateful Components #### 4. web/src/header.ts (Theme Dropdown) **Add import** (at top): ```typescript import { Alpine } from './alpine'; import { changeThemeTo, loadTheme, applyTheme } from './theme'; ``` **Add Alpine component** (at end of file): ```typescript Alpine.data('themeDropdown', () => ({ open: false, toggle() { this.open = !this.open; }, changeTheme(theme: string) { changeThemeTo(theme); this.open = false; }, init() { const savedTheme = loadTheme(); if (savedTheme) applyTheme(savedTheme); } })); ``` **Template usage**: ```html
``` #### 5. web/src/woodPaneling.ts **Add Alpine component** (at end of file): ```typescript import { Alpine } from './alpine'; Alpine.data('woodPaneling', () => ({ current: localStorage.getItem('woodPaneling') || 'none', change(style: string) { this.current = style; localStorage.setItem('woodPaneling', style); // Apply logic here... } })); ``` **Template usage**: ```html
``` ### Priority 3: Page-Specific Functions #### 6. web/src/collections.ts **Add import**: ```typescript import { Alpine } from './alpine'; ``` **Register as namespace** (at end): ```typescript Alpine.global('collections', { back: backToCollections, showAddModal: showAddBooksModal, hideAddModal: hideAddBooksModal, addSelected: addSelectedBooks, removeBook: removeBook, navigate: navigateToCollection }); ``` #### 7. web/src/devices.ts, queue.ts, conflicts.ts **Follow same pattern as collections.ts** - register as namespace with Alpine.global() --- ## Phase 6: Clean Up (Optional) ### Remove Individual JS Files After verifying everything works, remove old individual .js files: ```bash rm web/static/api.js rm web/static/events.js rm web/static/dom.js rm web/static/toast.js rm web/static/storage.js rm web/static/theme.js rm web/static/header.js rm web/static/themeDropdown.js rm web/static/woodPaneling.js rm web/static/search.js rm web/static/docs.js rm web/static/collections.js rm web/static/conflicts.js rm web/static/dashboard.js rm web/static/admin*.js rm web/static/analytics.js rm web/static/bookshelf.js rm web/static/devices.js rm web/static/index.js rm web/static/login.js rm web/static/profile.js rm web/static/queue.js rm web/static/custom_section.js rm web/static/progress.js rm web/static/register.js rm web/static/settings.js rm web/static/stats.js rm web/static/sync.js rm web/static/testing_templ.js rm web/static/obsidian.js rm web/static/whatsapp.js rm web/static/midnight.js rm web/static/sunset.js ``` **Note**: Keep `main.js` and `main.js.map` --- ## Troubleshooting ### Issue: "Alpine is not defined" **Cause**: Alpine not initialized or alpine.ts not imported in main.ts **Fix**: Ensure `import './alpine';` is at end of main.ts and Alpine.start() is called ### Issue: "Alpine.global is not a function" **Cause**: Using old Alpine syntax **Fix**: Use `Alpine.data()` for components or register globals before `Alpine.start()` ### Issue: "lunr is not defined" **Cause**: Missing import in docs.ts **Fix**: Ensure `import * as lunr from 'lunr';` is at line 4 ### Issue: "hljs is not defined" **Cause**: Missing import in docs.ts **Fix**: Ensure `import hljs from 'highlight.js';` is at line 5 ### Issue: `@click` handlers not working **Cause**: Alpine not loaded or syntax error in directive **Fix**: Check browser console, verify Alpine initialized, check directive syntax ### Issue: `x-show` elements always visible **Cause**: Missing `x-cloak` CSS or Alpine not loaded before DOM ready **Fix**: Add `[x-cloak] { display: none !important; }` to CSS, ensure Alpine loads early ### Issue: Bundle too large (>200KB minified) **Cause**: Check if large dependencies accidentally bundled **Fix**: Verify Chart.js is NOT bundled (should remain CDN link in analytics.templ) ### Issue: 404 errors for .js files **Cause**: Script tags not updated in templates **Fix**: Check that all template script tags point to `/static/main.js` ### Issue: Theme dropdown doesn't close **Cause**: Missing `@click.away` directive **Fix**: Add `@click.away="open = false"` to dropdown element ### Issue: Search not working on docs page **Cause**: lunr not properly imported or bundled **Fix**: Check docs.ts imports and rebuild with `npm run build:ts` ### Issue: Modals don't open **Cause**: `x-show` variable not reactive or parent `x-data` missing **Fix**: Ensure modal is wrapped in `x-data="{ modalOpen: false }"` and button uses `@click="modalOpen = true"` ### Issue: HTMX forms stopped working after Alpine migration **Cause**: Alpine event handlers conflicting with HTMX **Fix**: HTMX and Alpine work together - ensure `hx-post` is on form, `@click` is on buttons (not form) --- ## External Dependencies (Not Bundled) ### Chart.js **File**: `templates/analytics.templ` (line 14) **Reason**: 3.4MB minified, only used on 1 page (analytics) **Approach**: Keep as CDN link: `` ### htmx.org **File**: Loaded via separate script tag in base templates **Reason**: Core framework, needs to load before main.js **Approach**: Keep as separate script tag (not bundled), works great with Alpine --- ## Architecture Notes ### Why Alpine.js Over Window Exports? **Alpine is the modern approach** for this codebase: 1. **Reactive state management**: `x-data`, `x-show`, `x-model` instead of manual DOM manipulation 2. **Clean templates**: `@click` instead of `onclick="window.func()"` 3. **Component-based**: `Alpine.data()` for reusable components 4. **SSR-friendly**: Works with progressive enhancement 5. **No global pollution**: Functions registered in Alpine scope, not window 6. **Better DX**: Alpine DevTools for debugging reactive state ### Hybrid Approach: Alpine + HTMX - **Alpine**: Client-side state (modals, dropdowns, theme, form validation) - **HTMX**: Server calls (form submissions, API calls, data fetching) - **Why both**: HTMX excels at server communication, Alpine excels at client-side state ### ESBuild + Alpine Benefits - **Single bundle**: Alpine bundled with app code (~15KB gzipped) - **No CDN dependency**: Faster load, no network request for Alpine - **Tree-shaking**: Unused Alpine features not included - **ES2020 target**: Works on Chrome 80+, Firefox 72+, Safari 13.1+ ### Migration Path: Window → Alpine **Old approach**: ```typescript (window as any).showToast = showToast; ``` ```html ``` **New approach**: ```typescript Alpine.global('showToast', showToast); ``` ```html ``` ### Why Not Put Everything in main.ts? Main.ts stays simple (24 lines) because: - **Imports where used**: docs.ts imports lunr, not main.ts - **Tree-shaking**: Only what's used gets bundled - **Maintainability**: Each file has its own dependencies - **Testing**: Can test individual modules independently - **Alpine registration**: Each file registers its own Alpine globals/components --- ## Success Criteria ✅ Single `main.js` bundle (~130KB minified with Alpine) ✅ All 27 templates use single script tag ✅ No 404 errors for missing .js files ✅ Alpine.js loaded and functional ✅ No `(window as any)` usage in TypeScript ✅ Lunr search works on docs page ✅ Syntax highlighting works on docs page ✅ Theme dropdown opens/closes with Alpine ✅ Modals use `x-show`/`x-transition` ✅ All `@click` handlers work (151 migrated) ✅ Toast notifications work via Alpine ✅ HTMX forms still submit correctly ✅ Chart.js loads on analytics page (CDN) ✅ Browser console shows no errors ✅ Alpine DevTools shows reactive components ✅ Bundle targets ES2020 for broad compatibility ✅ Sourcemap generated for debugging --- ## Post-Migration: Development Workflow ### Watch Mode (Development) ```bash npm run dev ``` Watches both TypeScript and templates, rebuilds on changes. ### Production Build ```bash npm run build:ts templ generate go build ``` Creates minified bundle, regenerates templates, builds Go binary. ### Debugging Use `main.js.map` sourcemap in browser DevTools to debug original TypeScript sources. --- ## Files Modified Summary ### New Files Created - `web/src/alpine.ts` (new) - Alpine initialization and exports ### Configuration Files - ✅ package.json (already correct - no changes needed) - ✅ tsconfig.json (already ES2020 - no changes needed) ### Source Files Modified - `web/src/main.ts` (add `import './alpine'` at end) - `web/src/docs.ts` (remove 4 window globals, add Alpine registration) - `web/src/toast.ts` (replace window export with Alpine.global) - `web/src/api.ts` (replace window export with Alpine.global) - `web/src/storage.ts` (replace window export with Alpine.global) - `web/src/events.ts` (replace window export with Alpine.global) - `web/src/header.ts` (add Alpine.data themeDropdown component) - `web/src/woodPaneling.ts` (add Alpine.data component) - `web/src/collections.ts` (register namespace with Alpine.global) - `web/src/conflicts.ts` (register namespace with Alpine.global) - `web/src/devices.ts` (register namespace with Alpine.global) - `web/src/queue.ts` (register namespace with Alpine.global) - `web/src/search.ts` (register namespace with Alpine.global) - `web/src/dom.ts` (register namespace with Alpine.global) - Plus 10+ more TypeScript files (register functions with Alpine) ### Template Files Modified (27 files) All templates updated to: 1. Remove individual script tags 2. Use single `` 3. Replace `onclick` with `@click` 4. Add `x-data` for stateful components (modals, dropdowns) Templates: 1. templates/collections.templ (migrate 7 onclick handlers) 2. templates/docs.templ (migrate sidebar toggle to Alpine) 3. templates/admin.templ 4. templates/admin_library.templ 5. templates/dashboard.templ (theme dropdown, wood paneling) 6. templates/header.templ (theme dropdown, user menu - KEY FILE) 7. templates/analytics.templ (keep Chart.js CDN) 8. templates/bookshelf.templ 9. templates/devices.templ (multiple modals) 10. templates/index.templ 11. templates/login.templ 12. templates/profile.templ 13. templates/queue.templ 14. templates/conflicts.templ 15. templates/custom_section.templ 16. templates/admin_users.templ 17. templates/collection_rules.templ 18. templates/progress.templ 19. templates/register.templ 20. templates/settings.templ 21. templates/stats.templ 22. templates/sync.templ 23. templates/testing_templ.templ 24. themes/obsidian.templ 25. themes/whatsapp.templ 26. themes/midnight.templ 27. themes/sunset.templ ### Generated Files - `web/static/main.js` (new bundled output with Alpine) - `web/static/main.js.map` (new sourcemap) ### Files to Delete (After Verification) All individual .js files in `web/static/` (except main.js and main.js.map): - api.js, events.js, dom.js, toast.js, storage.js - theme.js, header.js, themeDropdown.js, woodPaneling.js - docs.js, search.js, collections.js, conflicts.js - dashboard.js, admin*.js, analytics.js, bookshelf.js - devices.js, index.js, login.js, profile.js, queue.js - custom_section.js, progress.js, register.js, settings.js - stats.js, sync.js, testing_templ.js - obsidian.js, whatsapp.js, midnight.js, sunset.js --- ## Migration Checklist Use this checklist to track progress: ### Phase 1: Clean Up docs.ts - [ ] Add imports for lunr and hljs (lines 3-4) - [ ] Remove `(window as any).lunr` check (line 50) - [ ] Replace `(window as any).lunrIndex` with `lunr.Builder.loadJs` (line 56) - [ ] Replace `(window as any).docsData` with `docs` import (line 73) - [ ] Replace window export with Alpine registration (line 99) ### Phase 2: Create Alpine Infrastructure - [ ] Create `web/src/alpine.ts` with Alpine initialization - [ ] Add `import './alpine'` to `web/src/main.ts` - [ ] Build and verify Alpine is loaded (`npm run build:ts`) ### Phase 3: Migrate Core TypeScript Files (Priority 1) - [ ] `web/src/toast.ts` - Register with Alpine.global - [ ] `web/src/api.ts` - Register with Alpine.global - [ ] `web/src/storage.ts` - Register with Alpine.global - [ ] `web/src/events.ts` - Register with Alpine.global - [ ] `web/src/dom.ts` - Register with Alpine.global ### Phase 4: Migrate Stateful Components (Priority 2) - [ ] `web/src/header.ts` - Create themeDropdown Alpine.data component - [ ] `web/src/woodPaneling.ts` - Create woodPaneling Alpine.data component - [ ] Test theme dropdown in browser - [ ] Test wood paneling in browser ### Phase 5: Migrate Page-Specific Files (Priority 3) - [ ] `web/src/collections.ts` - Register namespace - [ ] `web/src/devices.ts` - Register namespace - [ ] `web/src/queue.ts` - Register namespace - [ ] `web/src/conflicts.ts` - Register namespace - [ ] `web/src/search.ts` - Register namespace ### Phase 6: Update Templates (27 files) - [ ] templates/header.templ (CRITICAL - theme dropdown) - [ ] templates/docs.templ (sidebar toggle) - [ ] templates/dashboard.templ (wood paneling) - [ ] templates/collections.templ (7 onclick handlers) - [ ] templates/admin.templ through themes/sunset.templ (23 remaining) - [ ] Run `templ generate` after template changes ### Phase 7: Test and Verify - [ ] Build: `npm run build:ts` - [ ] Check bundle size: ~130KB minified - [ ] Start dev server: `go run .` - [ ] Test all 151 onclick handlers - [ ] Verify Alpine DevTools shows components - [ ] Test HTMX forms still work - [ ] Check browser console for errors - [ ] Test on Chrome, Firefox, Safari (ES2020 compatibility) ### Phase 8: Clean Up - [ ] Delete all individual .js files from web/static/ - [ ] Commit changes - [ ] Deploy and test in production --- ## Additional Resources ### Alpine.js Documentation - Official docs: https://alpinejs.dev/ - Essentials guide: https://alpinejs.dev/essentials/start - `x-data` docs: https://alpinejs.edu/directives/data - `@click` docs: https://alpinejs.edu/directives/on - `x-show` docs: https://alpinejs.edu/directives/show - `x-transition` docs: https://alpinejs.edu/directives/transition ### Alpine + HTMX Integration - Blog post: https://htmx.org/examples/blog/ - Both work together seamlessly - Alpine for client state, HTMX for server calls ### ESBuild Documentation - Official docs: https://esbuild.github.io/ - API: https://esbuild.github.io/api/ - Bundling: https://esbuild.github.io/api/#bundle ### Migration Tips 1. **Start small**: Migrate 1-2 files at a time, test frequently 2. **Use Alpine DevTools**: Install browser extension for debugging 3. **Keep HTMX for forms**: Don't replace `hx-post` with Alpine fetch 4. **Test in browser**: Console errors will show missing Alpine globals 5. **Progressive migration**: Can use window exports AND Alpine during transition ### Configuration - ✅ package.json (already correct) - ✅ tsconfig.json (already ES2020) ### Source Files - `web/src/docs.ts` (add imports, remove 4 window globals) ### Template Files (27 files) All templates updated to use single `` tag: 1. templates/collections.templ 2. templates/docs.templ 3. templates/admin.templ 4. templates/admin_library.templ 5. templates/dashboard.templ 6. templates/header.templ 7. templates/analytics.templ 8. templates/bookshelf.templ 9. templates/devices.templ 10. templates/index.templ 11. templates/login.templ 12. templates/profile.templ 13. templates/queue.templ 14. templates/conflicts.templ 15. templates/custom_section.templ 16. templates/admin_users.templ 17. templates/collection_rules.templ 18. templates/progress.templ 19. templates/register.templ 20. templates/settings.templ 21. templates/stats.templ 22. templates/sync.templ 23. templates/testing_templ.templ 24. themes/obsidian.templ 25. themes/whatsapp.templ 26. themes/midnight.templ 27. themes/sunset.templ ### Generated Files - `web/static/main.js` (new bundled output) - `web/static/main.js.map` (new sourcemap) ### Files to Delete (After Verification) All individual .js files in `web/static/` (except main.js and main.js.map) --- ## Summary: What We're Achieving ### The Problem - 151 inline `onclick` handlers using `window.funcName()` - 20+ TypeScript files exporting to `(window as any)` - 27 template files with 3-5 script tags each - Manual DOM manipulation for state (modals, dropdowns) - No reactive state management - Global namespace pollution ### The Solution 1. **Bundle with ESBuild**: Single ~130KB minified bundle (ES2020 target) 2. **Alpine.js for state**: Reactive components with `x-data`, `@click`, `x-show` 3. **Keep HTMX for forms**: Server-side communication remains unchanged 4. **Modern patterns**: No more `(window as any)`, clean templates ### Benefits - ✅ **Performance**: Single HTTP request for all JS, better caching - ✅ **Maintainability**: Alpine components vs imperative DOM manipulation - ✅ **Type safety**: No more `window as any` hacks - ✅ **Developer experience**: Clean templates, Alpine DevTools, sourcemaps - ✅ **User experience**: Smooth transitions, reactive UI, faster loads - ✅ **Bundle size**: Only ~40KB gzipped (was 30+ HTTP requests before) ### Key Architecture Decisions 1. **Alpine over vanilla event listeners**: Reactive state is cleaner 2. **Alpine over React/Vue**: Lightweight, SSR-friendly, works with HTMX 3. **Bundle Alpine**: No CDN dependency, faster load, tree-shaking 4. **Keep HTMX**: Don't fix what works - HTMX excels at server communication 5. **Progressive migration**: Migrate incrementally, can mix old/new during transition ### Migration Effort - **TypeScript files**: 20+ files (5-10 min each) = ~3 hours - **Templates**: 27 files (10-15 min each) = ~5 hours - **Testing**: 2-3 hours - **Total**: ~10-12 hours for complete migration ### Maintenance Going Forward - Adding new feature? Create Alpine component or register function - New page? Single `