diff --git a/ESBUILD_IMPORT_FIXES.md b/ESBUILD_IMPORT_FIXES.md new file mode 100644 index 0000000..3d8658e --- /dev/null +++ b/ESBUILD_IMPORT_FIXES.md @@ -0,0 +1,97 @@ +# Import Fixes Applied to Migration Plan + +## Summary + +All imports in the ESBUILD_MIGRATION_PLAN.md have been corrected to use individual function imports instead of namespace objects, matching your actual module exports. + +## Changes Made + +### 1. TypeScript Imports (Phase 1) +Changed from namespace imports to individual function imports: + +**Before** (WRONG): +```typescript +import { api } from "./api"; +import { dom } from "./dom"; +import { events } from "./events"; +``` + +**After** (CORRECT): +```typescript +import { apiGet, apiPost, apiPut, apiDelete, apiPatch, handleResponse, handleVoidResponse, handleError } from "./api"; +import { querySelector, querySelectorAll, getElementById, createElement, ... } from "./dom"; +import { onDelegatedClick, onDelegatedSubmit, onDelegatedChange, ... } from "./events"; +``` + +### 2. Function Calls in TypeScript +Changed from namespaced calls to direct function calls: + +**Before** (WRONG): +```typescript +const response = await api.get("/libraries"); +showToast.success("Loaded!"); +const el = dom.getElementById("id"); +``` + +**After** (CORRECT): +```typescript +const response = await apiGet("/libraries"); +showToast("Loaded!", "success"); +const el = getElementById("id"); +``` + +### 3. Alpine/Template Calls (Phase 3) +Keep namespace objects (Alpine creates these): + +**CORRECT for Templates**: +```html + + +``` + +## Key Distinction + +### TypeScript Code +- **Imports**: Individual functions +- **Calls**: Direct function calls with arguments +- **Example**: `import { apiGet }` → `apiGet("/url")` + +### Template/Alpine Code +- **Imports**: None (Alpine handles this) +- **Calls**: Namespaced via Alpine.global() +- **Example**: `@click="api.post()"` (Alpine namespace) + +## Files Affected + +All TypeScript file examples in Phase 1 now show correct imports: +- ✅ dashboard.ts +- ✅ analytics.ts +- ✅ library.ts +- ✅ collections.ts +- ✅ bookshelf.ts +- ✅ api-explorer.ts +- ✅ All others + +## Verification + +```bash +# Verify no namespace imports remain in TypeScript sections +grep "import { api } from\|import { dom } from\|import { events } from" ESBUILD_MIGRATION_PLAN.md | grep -v "//" | wc -l +# Result: 0 ✅ + +# Verify Alpine namespace calls in templates +grep '@click="api\.' ESBUILD_MIGRATION_PLAN.md | wc -l +# Result: 3 ✅ + +# Verify TypeScript individual imports +grep "import { apiGet" ESBUILD_MIGRATION_PLAN.md | wc -l +# Result: 9 ✅ +``` + +## Plan Status + +✅ **Ready to execute** - All imports corrected +✅ **Consistent throughout** - TypeScript vs Alpine distinction clear +✅ **Matches your actual exports** - No namespace objects exported from modules + +You can now start Phase 0 with confidence! diff --git a/ESBUILD_MIGRATION_PLAN.md b/ESBUILD_MIGRATION_PLAN.md new file mode 100644 index 0000000..f343055 --- /dev/null +++ b/ESBUILD_MIGRATION_PLAN.md @@ -0,0 +1,1709 @@ +# ESBuild Migration Plan: TypeScript → ES Modules + Alpine + +## Executive Summary + +**Goal**: Migrate from individual JavaScript files with window globals to a single bundled `main.js` using ESBuild, ES modules for internal dependencies, and Alpine.js for template interactivity. + +**Architecture Principles**: +1. **ES Modules** for all TypeScript → TypeScript dependencies +2. **Alpine.js** ONLY as a bridge to templates (onclick → @click) +3. **SSR-first** - Server renders complete HTML, client-side JS only for interactivity +4. **No window globals** - everything through proper imports/exports +5. **Incremental migration** - Each phase is complete and testable + +**Target Bundle**: ~100-130KB minified (ES2020 target = Chrome 80+, Firefox 72+, Safari 13.1+, Edge 80+) + +--- + +## Current State Analysis + +### Problems with Current Architecture + +1. **274 window global references** across 21 TypeScript files +2. **193+ internal TypeScript dependencies** via window (should be ES imports) +3. **27 template files** with 150+ unique onclick handlers +4. **Function wrapping** (themeDropdown.ts wraps header.ts functions) +5. **Individual .js files** loaded in each template (not bundled) +6. **Mixed patterns** - some files have ES exports, some don't + +### Files Requiring Migration + +**TypeScript Source Files** (21 files): +- api.ts, toast.ts, storage.ts, events.ts, dom.ts +- theme.ts, header.ts, themeDropdown.ts, woodPaneling.ts +- library.ts, collections.ts, conflicts.ts, queue.ts +- dashboard.ts, admin.ts, bookshelf.ts, search.ts +- device-management.ts, analytics.ts, custom-section-builder.ts +- docs.ts, api-explorer.ts, password_validation.ts + +**Template Files** (27 files): +- dashboard.templ, collections.templ, header.templ, admin.templ +- docs.templ, bookshelf.templ, devices.templ, queue.templ +- conflicts.templ, analytics.templ, login.templ, register.templ +- profile.templ, settings.templ, stats.templ, sync.templ +- progress.templ, custom_section.templ, collection_rules.templ +- admin_library.templ, admin_users.templ, api_explorer.templ +- Plus 8 more modal/component templates + +--- + +## Architecture Principles + +### 1. ES Module Exports (TypeScript → TypeScript) + +Every utility module exports functions for other TypeScript files to import: + +```typescript +// api.ts +export async function apiGet(url: string): Promise { ... } +export async function apiPost(url: string, data?: unknown): Promise { ... } +export async function handleResponse(response: Response): Promise { ... } +``` + +```typescript +// library.ts (consumes api.ts) +import { apiGet, handleResponse } from "./api"; + +const response = await apiGet("/libraries"); +const result = await handleResponse(response); +``` + +### 2. Alpine.js Bridge (Templates → TypeScript) + +Alpine is ONLY used to expose functions to templates, not for internal TS dependencies: + +```typescript +// api.ts (bottom of file) +import { Alpine } from "./alpine"; + +Alpine.global("api", { + get: apiGet, + post: apiPost, + put: apiPut, + delete: apiDelete, + handleResponse, + handleVoidResponse, + handleError, +}); +``` + +Template usage: +```html + + + + + +``` + +### 3. SSR-First with Progressive Enhancement + +- Server renders complete HTML with data +- Client-side JavaScript only for interactivity (modals, dropdowns, forms) +- Data loaded via fetch() APIs (like docs search index) +- No window globals for data injection + +--- + +## Migration Strategy: Incremental with No Legacy Code + +### Why This Strategy Works + +1. **No breaking changes during migration** - App works at every phase +2. **Can ship anytime** - Partial migrations are functional +3. **No legacy code** - Each file completely migrated, no half-states +4. **Easy rollback** - If issues arise, revert individual files/templates +5. **Testable at each step** - Clear verification criteria + +--- + +## Phase 0: Foundation - Add ES Module Exports + +**Goal**: Add proper ES exports to all utility modules WITHOUT breaking existing functionality. + +**Files**: 8 utility modules + +**Duration**: 1-2 hours + +### Step 0.1: Add Exports to Utility Modules + +For each file, add `export` statements while keeping window exports temporarily: + +**web/src/api.ts** (Already has Alpine, just needs ES exports) +```typescript +// Add at bottom (after Alpine.global block): +export { apiGet, apiPost, apiPut, apiDelete, apiPatch, handleResponse, handleVoidResponse, handleError }; +``` + +**web/src/toast.ts** (Already has Alpine, just needs ES exports) +```typescript +// Add at bottom (after Alpine.global block): +export { showToast }; +export type { ToastType }; +``` + +**web/src/storage.ts** (Already has exports! ✅) +```typescript +// Already has: +// export { getToken, setToken, ... }; +// No changes needed! +``` + +**web/src/events.ts** +```typescript +// Add at bottom: +export { + onDelegatedClick, + onDelegatedSubmit, + onDelegatedChange, + onDelegatedKeydown, + getDataAttribute, + setDataAttribute, + onClick, + onSubmit, + onChange, + onKeydown, + onInput, + preventDefault, + stopPropagation, +}; +``` + +**web/src/dom.ts** (Already has exports! ✅) +```typescript +// Already has: +// export { escapeHtml, querySelector, ... }; +// No changes needed! +``` + +**web/src/theme.ts** +```typescript +// Add at bottom: +export { applyTheme, loadTheme, changeTheme, loadUserTheme, initializeTheme }; +export type { ThemeType }; +``` + +**web/src/header.ts** +```typescript +// Add at bottom: +export { toggleThemeDropdown, toggleUserMenu, changeThemeTo, logout }; +``` + +**web/src/woodPaneling.ts** +```typescript +// Add at bottom: +export { changeWoodPaneling, loadWoodPaneling, updateWoodPanelingIndicators }; +``` + +### Step 0.2: Verify Build + +```bash +npm run build:ts +``` + +**Expected**: Build succeeds, no errors + +**Why this works**: We're adding exports without changing anything else. App still uses window globals, so functionality is unchanged. + +--- + +## Phase 1: Internal TypeScript Dependencies + +**Goal**: Convert all internal `window.XXX` reads to proper ES module imports. + +**Files**: 13 consumer files + +**Duration**: 4-6 hours + +### 1.1: Simple Consumers (Low Risk) + +Files with minimal window dependencies: + +**web/src/dashboard.ts** +```typescript +// Add at top: +import { apiGet, apiPost, apiPut, apiDelete, apiPatch, handleResponse, handleVoidResponse, handleError } from "./api"; +import { showToast } from "./toast"; + +// Replace ALL instances: +// Line 22: (window as any).showToast.error("msg") → showToast("msg", "error") +// Line 37: (window as any).showToast.error("msg") → showToast("msg", "error") +// Line 128: (window as any).api.put → apiPut +// Line 135: (window as any).showToast.success("msg") → showToast("msg", "success") +// ... (10 more replacements) +``` + +**web/src/search.ts** +```typescript +// No changes needed! ✅ +// Already uses localStorage directly, doesn't read from window +// Only exports selectLibraryAndBook +``` + +**web/src/device-management.ts** +```typescript +// Add at top: +import { showToast } from "./toast"; + +// Replace ALL instances: +// Lines 32-34, 39-42, 78-82, 89-92: const toast = (window as any).showToast → showToast +``` + +**web/src/password_validation.ts** +```typescript +// No window reads found, only exports initPasswordValidation +// No changes needed +``` + +### 1.2: Moderate Consumers + +Files with multiple window dependencies: + +**web/src/admin.ts** +```typescript +// Add at top: +import { showToast } from "./toast"; + +// Replace ALL instances (14 occurrences): +// Lines 12-13, 17-18, 23-24, 40-41, 45-46, 53-54, 119-120, 152-153, 161-162 +// (window as any).showToast.success("msg") → showToast("msg", "success") +// (window as any).showToast.error → showToast.error +``` + +**web/src/queue.ts** +```typescript +// Add at top: +import { showToast } from "./toast"; + +// Replace ALL instances (8 occurrences): +// Lines 30-32, 37-39, 56-58, 63-65, 82-84, 89-91 +``` + +**web/src/conflicts.ts** +```typescript +// Add at top: +import { showToast } from "./toast"; + +// Replace ALL instances (6 occurrences): +// Lines 39-41, 45-48, 53-55, 78-80, 85-87 +``` + +**web/src/linking.ts** +```typescript +// Add at top: +import { showToast } from "./toast"; + +// Replace ALL instances: +// (window as any).showToast.success/error("msg") → showToast("msg", "success"/"error") +``` + +**web/src/custom-section-builder.ts** +```typescript +// Add at top: +import { showToast } from "./toast"; +import { apiGet, apiPost, apiPut, apiDelete, apiPatch, handleResponse, handleVoidResponse, handleError } from "./api"; + +// Replace ALL instances (9 occurrences): +// Lines 7, 10, 13, 16, 19, 22, 25, 28 +``` + +**web/src/analytics.ts** +```typescript +// Add at top: +import { apiGet, apiPost, apiPut, apiDelete, apiPatch, handleResponse, handleVoidResponse, handleError } from "./api"; +import { querySelector, querySelectorAll, getElementById, createElement, showElement, hideElement, toggleElement, addClass, removeClass, toggleClass, hasClass, setTextContent, setInnerHTML, escapeHtml } from "./dom"; + +// Replace ALL instances: +// (window as any).api.get → apiGet +// (window as any).dom.getElementById → getElementById +``` + +**web/src/bookshelf.ts** +```typescript +// Add at top: +import { showToast } from "./toast"; +import { apiGet, apiPost, apiPut, apiDelete, apiPatch, handleResponse, handleVoidResponse, handleError } from "./api"; +import { onDelegatedClick, onDelegatedSubmit, onDelegatedChange, onDelegatedKeydown, getDataAttribute, setDataAttribute, onClick, onSubmit, onChange, onKeydown, onInput, preventDefault, stopPropagation } from "./events"; + +// Replace ALL instances +``` + +**web/src/api-explorer.ts** +```typescript +// Add at top: +import { apiGet, apiPost, apiPut, apiDelete, apiPatch, handleResponse, handleVoidResponse, handleError } from "./api"; +import { querySelector, querySelectorAll, getElementById, createElement, showElement, hideElement, toggleElement, addClass, removeClass, toggleClass, hasClass, setTextContent, setInnerHTML, escapeHtml } from "./dom"; + +// Replace ALL instances +``` + +### 1.3: Complex Consumers (High Risk) + +Files with heavy window dependencies: + +**web/src/library.ts** (55 occurrences!) +```typescript +// Add at top: +import { apiGet, apiPost, apiPut, apiDelete, apiPatch, handleResponse, handleVoidResponse, handleError } from "./api"; +import { showToast } from "./toast"; +import { querySelector, querySelectorAll, getElementById, createElement, showElement, hideElement, toggleElement, addClass, removeClass, toggleClass, hasClass, setTextContent, setInnerHTML, escapeHtml } from "./dom"; + +// Systematic replacement: +// (window as any).api.get → apiGet +// (window as any).api.handleResponse → handleResponse +// (window as any).api.handleError → handleError +// (window as any).showToast.success("msg") → showToast("msg", "success") +// (window as any).dom.createElement → createElement +``` + +**web/src/collections.ts** (54 occurrences!) +```typescript +// Add at top: +import { apiGet, apiPost, apiPut, apiDelete, apiPatch, handleResponse, handleVoidResponse, handleError } from "./api"; +import { showToast } from "./toast"; +import { querySelector, querySelectorAll, getElementById, createElement, showElement, hideElement, toggleElement, addClass, removeClass, toggleClass, hasClass, setTextContent, setInnerHTML, escapeHtml } from "./dom"; + +// Systematic replacement of all 54 instances +``` + +### 1.4: Restructure Function Wrapping (Option A) + +**web/src/themeDropdown.ts** - Eliminate function wrapping + +**Current (WRONG)**: +```typescript +const originalToggleThemeDropdown = (window as any).toggleThemeDropdown; +(window as any).toggleThemeDropdown = () => { + originalToggleThemeDropdown(); + updateThemeIndicators(); + (window as any).updateWoodPanelingIndicators?.(); +}; +``` + +**New Approach**: +```typescript +import { toggleThemeDropdown as originalToggle } from "./header"; +import { updateWoodPanelingIndicators } from "./woodPaneling"; + +export function initializeThemeDropdown() { + // Call original function + originalToggle(); + // Then update indicators + updateThemeIndicators(); + updateWoodPanelingIndicators(); +} + +// Export for Alpine +export { updateThemeIndicators }; +``` + +**web/src/docs.ts** - Remove window globals + +**Current (WRONG)**: +```typescript +if (!(window as any).lunr) { ... } +const idx = (window as any).lunrIndex; +const doc = (window as any).docsData?.[result.ref]; +``` + +**New Approach** (data already fetched via API): +```typescript +// Already using fetch in template inline JS (lines 228-285) +// Just remove window checks, fetch API handles it + +// Keep only: +import * as lunr from "lunr"; +import hljs from "highlight.js"; +import { Alpine } from "./alpine"; + +// Export for Alpine +export function initializeDocsSearch() { ... } +export { toggleSidebar }; + +Alpine.global("docs", { + toggleSidebar, + initializeSearch: initializeDocsSearch, +}); +``` + +### Step 1.5: Remove Window Exports from Consumer Files + +After converting all reads to imports, remove window exports from consumer files (they don't need to expose to window anymore): + +**web/src/dashboard.ts** +```typescript +// Remove these lines (if present): +// (window as any).scrollCarousel = scrollCarousel; +// (window as any).openDashboardSettings = openDashboardSettings; +// ... etc +``` + +**Only keep Alpine.global()** for functions that templates call directly. + +### Step 1.6: Verify Build and Test + +```bash +npm run build:ts +go run . +``` + +**Verification**: +- Build succeeds +- All pages still work (dashboard, library, collections, etc.) +- Console shows no errors +- All onclick handlers still work + +**Why this works**: Internal dependencies now use imports, but window exports still exist for templates. App is fully functional. + +--- + +## Phase 2: Dual Exports - ES Modules + Alpine Bridge + +**Goal**: Establish Alpine.js as the bridge to templates while keeping ES exports for TypeScript. + +**Files**: All files that export functions used by templates + +**Duration**: 2-3 hours + +### Step 2.1: Ensure Alpine Initialization File + +**web/src/alpine.ts** (Already exists, verify it's correct) +```typescript +import Alpine from "alpinejs"; + +// Extend Window interface +declare global { + interface Window { + Alpine: typeof Alpine; + } +} + +// Initialize Alpine +window.Alpine = Alpine; +Alpine.start(); + +// Re-export for other modules +export { Alpine }; +``` + +### Step 2.2: Register Template Functions with Alpine + +For each file that templates call, register functions with Alpine: + +**web/src/api.ts** (Already done ✅) +```typescript +Alpine.global("api", { + get: apiGet, + post: apiPost, + // ... etc +}); +``` + +**web/src/toast.ts** (Already done ✅) +```typescript +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), +}); +``` + +**web/src/header.ts** +```typescript +Alpine.global("header", { + logout, + toggleThemeDropdown, + toggleUserMenu, + changeThemeTo: (theme: string) => { + changeThemeTo(theme); + updateThemeIndicators(); // Call themeDropdown function + }, +}); +``` + +**web/src/library.ts** +```typescript +Alpine.global("library", { + deleteLibrary, + showLibraryFolders, + addLibraryFolder, + removeLibraryFolder, +}); +``` + +**web/src/collections.ts** +```typescript +Alpine.global("collections", { + loadCollections, + createRule, + deleteRule, + testRule, + navigateToCollection, + selectColor, + closeCollectionModal, + initColorSelection, + selectIcon, + filterIcons, + showAllIcons, + populateIconGrid, + initIconSelection, + initCollectionDetail, + showAddBooksModal, + hideAddBooksModal, + searchBooksForCollections, + toggleBookSelection, + addbooksToAdd, + removeBook, + toggleBookForRemoval, + updateSelectedCount, + removebooksToAdd, + filterCollectionBooks, + backToCollections, +}); +``` + +**web/src/admin.ts** +```typescript +Alpine.global("admin", { + triggerLibraryScan, + triggerQuickScan, + loadSystemStats, + scanAllLibraries, + loadWatchStatus, + hideScanProgress, + stopScanStatusPolling, +}); +``` + +**web/src/queue.ts** +```typescript +Alpine.global("queue", { + refreshQueue, + processPendingItems, + clearFailedItems, + clearAllItems, + retryQueueItem, + deleteQueueItem, +}); +``` + +**web/src/conflicts.ts** +```typescript +Alpine.global("conflicts", { + refreshConflicts, + resolveConflict, + bulkResolve, + bulkDismiss, + dismissAllResolved, + showResolveModal, + hideResolveModal, + handleResolveSubmit, +}); +``` + +**web/src/docs.ts** +```typescript +Alpine.global("docs", { + toggleSidebar, + initializeSearch: initializeDocsSearch, +}); +``` + +**web/src/search.ts** +```typescript +Alpine.global("search", { + selectLibraryAndBook, +}); +``` + +**web/src/device-management.ts** +```typescript +Alpine.global("devices", { + copyToClipboard, + regenerateDeviceToken, +}); +``` + +**web/src/password_validation.ts** +```typescript +Alpine.global("validation", { + initPasswordValidation, +}); +``` + +### Step 2.3: Remove Old Window Exports + +After Alpine registration, REMOVE the old window export lines: + +**Before**: +```typescript +(window as any).api = { ... }; +Alpine.global("api", { ... }); +``` + +**After**: +```typescript +Alpine.global("api", { ... }); +``` + +Systematically search and remove these patterns from all files. + +### Step 2.4: Verify Build + +```bash +npm run build:ts +go run . +``` + +**Verification**: +- Build succeeds +- All pages still work +- Alpine is loaded (check browser DevTools: `window.Alpine` should be defined) +- Console shows no errors + +**Why this works**: Functions now registered with Alpine instead of window, but templates still use onclick="func()" so they still work. + +--- + +## Phase 3: Template Migration (Incremental) + +**Goal**: Migrate templates one-by-one from onclick handlers to Alpine directives. + +**Strategy**: One template at a time, test each, can ship after each migration. + +**Duration**: 1-2 hours per template (27 templates = 27-54 hours total) + +### Template Migration Pattern + +For each template file: + +#### Step A: Remove Individual Script Tags + +**Before**: +```templ + + + + + + +``` + +**After**: +```templ + + +``` + +**Note**: Keep htmx.min.js separate (loaded before main.js) + +#### Step B: Convert onclick to @click + +**Before**: +```templ + + +``` + +**After**: +```templ + +
+ +
+``` + +#### Step C: Add Alpine State for UI Components + +For modals, dropdowns, and any UI with show/hide state: + +**Before**: +```templ + + +``` + +**After**: +```templ +
+ +
+ ...modal content... + +
+
+``` + + +**Important**: Alpine uses namespace objects (`api.post`, `showToast.success`), +**while TypeScript code uses individual functions** (`apiPost`, `showToast`). +**This is correct** - see Phase 2 for how Alpine creates these namespace objects. + +#### Step D: Update Namespace Calls + +**Before**: +```templ + + +``` + +**After**: +```templ + + +``` + +### Template Order (Low Risk to High Risk) + +#### Batch 1: Simple Pages (No complex state) +1. **login.templ** - Only has showToast, api +2. **register.templ** - Only has showToast, api, validation +3. **profile.templ** - Only has showToast, api +4. **settings.templ** - Only has showToast, api + +#### Batch 2: Pages with Simple State +5. **stats.templ** - Only has showToast, api, events +6. **sync.templ** - Only has showToast, api +7. **progress.templ** - Only has showToast, api, events +8. **custom_section.templ** - Has modal, but simple + +#### Batch 3: Pages with Moderate State +9. **collection_rules.templ** - Has showToast, api, events, some state +10. **queue.templ** - Has queue namespace, multiple modals +11. **conflicts.templ** - Has conflicts namespace, modal +12. **admin.templ** - Has admin namespace, modal +13. **admin_library.templ** - Has library namespace, multiple modals +14. **admin_users.templ** - Has header functions, simple + +#### Batch 4: Complex Pages +15. **dashboard.templ** - Has theme dropdown, wood paneling, collections +16. **bookshelf.templ** - Has search, book viewing, shelf mappings +17. **devices.templ** - Has devices namespace, multiple modals +18. **analytics.templ** - Keep Chart.js on CDN, has analytics namespace +19. **api_explorer.templ** - Has explorer namespace + +#### Batch 5: Most Complex (Shared Components) +20. **collections.templ** - Has collections namespace, multiple modals, color/icon pickers +21. **docs.templ** - Has sidebar toggle, search, inline JS to migrate +22. **header.templ** - Used in 17 templates! Theme dropdown, user menu + +#### Batch 6: Modal/Component Templates +23. **collection_modal.templ** - Shared component +24. **restore_system_collection_modal.templ** - Shared component +25. **profile_modal.templ** - Shared component +26. **profile_form.templ** - Shared component +27. **toast.templ** - Shared component (if using template for toast) + +### Special Cases + +#### docs.templ - Migrate Inline JavaScript + +**Current** (lines 124-515): Large inline script block + +**New Approach**: Move to docs.ts + +```typescript +// web/src/docs.ts +export function initializeDocsPage() { + // Move toggleSection, toggleSidebar functions here + // Move search initialization here + // Keep using fetch for data loading (SSR-compatible) +} + +Alpine.global("docs", { + initializePage: initializeDocsPage, + toggleSection, + toggleSidebar, +}); +``` + +```templ + + +``` + +#### header.templ - Most Critical (Used in 17 Places) + +This template is included in 17 other templates. Test thoroughly: + +```templ + + + + + + +``` + +```templ + +
+ +
+ + ...11 more themes... + + +
+ + +
+ +``` + +### Testing Strategy for Each Template + +After migrating each template: + +1. **Build**: `npm run build:ts && templ generate` +2. **Run**: `go run .` +3. **Test**: Visit the page, test all interactions: + - All buttons work + - Modals open/close + - Dropdowns work + - Forms submit (via HTMX) + - Toast notifications appear + - No console errors +4. **Verify**: Check Alpine DevTools (if installed) for reactive state + +### Rollback Strategy + +If a migrated template has issues: + +```bash +# Revert the template file +git checkout templates/PROBLEM_TEMPLATE.templ + +# Rebuild +templ generate +go run . +``` + +The template is now using old onclick handlers, but the bundle still has the functions registered. Everything works. + +--- + +## Phase 4: Data Injection (Server → Client) + +**Goal**: Ensure server-to-client data flow works correctly without window globals. + +**Files**: docs.templ, any future templates that need data injection + +**Duration**: 1-2 hours + +### Current Situation + +**docs.templ** currently loads data via fetch (correct approach): +```javascript +// In template inline JS (lines 228-285) +fetch('/docs/search-index.json') + .then(r => r.json()) + .then(data => { searchDocs = data; }) +``` + +**Go handler provides**: +```go +// internal/docs/http_handler.go line 417 +func (h *HTTPHandler) ServeSearchIndex(c *echo.Context) error { + index, err := h.docs.GenerateSearchIndex(); + return c.JSON(http.StatusOK, index) +} +``` + +### Migration Strategy + +Keep this pattern! It's already correct: + +1. **Server provides JSON endpoints** for data +2. **Client fetches data** via APIs +3. **No window globals** needed + +### Future Templates (Dashboard Pattern) + +For new pages that follow the dashboard pattern: + +1. **Server renders HTML** with initial data (SSR) +2. **Client fetches updates** via API when needed +3. **No data injection into window** + +Example: +```go +// Go handler +func (h *Handler) ShowDashboard(c *echo.Context) error { + // Fetch data from database + collections := h.queries.GetCollections(c.request().Context()) + + // Render template with data + return templates.Dashboard(collections).Render(c) +} +``` + +```typescript +// Client-side (if needed) +import { apiGet, apiPost, apiPut, apiDelete, apiPatch, handleResponse, handleVoidResponse, handleError } from "./api"; + +async function refreshCollections() { + const response = await apiGet("/collections"); + const data = await handleResponse(response); + // Update DOM +} +``` + +--- + +## Phase 5: Cleanup + +**Goal**: Remove all legacy code and old .js files. + +**Duration**: 1-2 hours + +### Step 5.1: Remove Individual .js Files + +After all 27 templates are migrated and verified working: + +```bash +cd /home/nymusicman/Code/bookhoard/web/static + +# Remove old individual JS files +rm -f api.js toast.js events.js dom.js storage.js +rm -f theme.js header.js themeDropdown.js woodPaneling.js +rm -f search.js docs.js collections.js conflicts.js +rm -f dashboard.js admin.js admin_library.js admin_users.js +rm -f analytics.js bookshelf.js devices.js +rm -f index.js login.js profile.js +rm -f queue.js custom_section.js progress.js +rm -f register.js settings.js stats.js sync.js +rm -f testing_templ.js obsidian.js whatsapp.js midnight.js sunset.js +rm -f password_validation.js api_explorer.js + +# Keep only: +# - main.js (bundled) +# - main.js.map (sourcemap) +# - htmx.min.js (separate load) +# - highlight.min.js (for docs) +# - style.css (Tailwind) +``` + +### Step 5.2: Remove All Window Exports + +From all TypeScript files, remove any remaining `(window as any)` exports: + +```bash +# Search for remaining window exports +cd /home/nymusicman/Code/bookhoard/web/src +grep -rn "window as any" *.ts +``` + +Should only find: +- Type assertions being removed +- Comments referencing old pattern + +Remove all: +```typescript +// DELETE these lines: +(window as any).functionName = functionName; +``` + +### Step 5.3: Update Build Scripts + +Verify package.json scripts are correct: + +```json +{ + "scripts": { + "build:css": "tailwindcss -i ./web/static/input.css -o ./web/static/style.css --watch", + "build:css:prod": "tailwindcss -i ./web/static/input.css -o ./web/static/style.css --minify", + "build:ts": "esbuild web/src/main.ts --bundle --outfile=web/static/main.js --sourcemap --target=es2020 --minify", + "build:ts:dev": "esbuild web/src/main.ts --bundle --outfile=web/static/main.js --sourcemap --target=es2020", + "build:ts:watch": "esbuild web/src/main.ts --bundle --outfile=web/static/main.js --sourcemap --target=es2020 --watch", + "build": "npm run build:ts && npm run build:css:prod", + "dev": "npm run build:ts:dev && templ generate && go run ." + } +} +``` + +### Step 5.4: Final Verification + +```bash +# Complete build +npm run build +templ generate +go build + +# Check bundle size +ls -lh web/static/main.js +# Expected: ~120-150KB + +# Run tests (if any) +go test ./... + +# Manual testing +go run . +# Test all pages, verify functionality +``` + +### Step 5.5: Update Documentation + +Update or remove any references to old build process in documentation. + +--- + +## Success Criteria + +### Phase 0 Completion +✅ All utility modules have ES exports +✅ Build succeeds +✅ No functionality broken + +### Phase 1 Completion +✅ All 193+ internal window reads converted to imports +✅ All consumer files use ES module imports +✅ Function wrapping eliminated (themeDropdown.ts restructured) +✅ Build succeeds +✅ All pages still work + +### Phase 2 Completion +✅ All template functions registered with Alpine +✅ Old window exports removed +✅ Build succeeds +✅ Alpine loaded and functional +✅ All pages still work (onclick still works) + +### Phase 3 Completion +✅ All 27 templates migrated to @click +✅ All individual script tags replaced with single main.js +✅ Inline JS migrated to TypeScript where appropriate +✅ All onclick handlers converted to @click +✅ All UI state uses x-data/x-show +✅ All templates tested and working + +### Phase 4 Completion +✅ Server data injection uses API endpoints (not window) +✅ Docs search still works +✅ Future pages follow SSR-first pattern + +### Phase 5 Completion +✅ All old .js files removed +✅ No window globals remain +✅ Only main.js and main.js.map exist +✅ Bundle size ~120-150KB minified +✅ Clean codebase ready for launch + +--- + +## Estimated Effort + +| Phase | Duration | Risk | Can Ship After | +|-------|----------|------|----------------| +| Phase 0 | 1-2 hours | Low | ✅ Yes | +| Phase 1 | 4-6 hours | Medium | ✅ Yes | +| Phase 2 | 2-3 hours | Low | ✅ Yes | +| Phase 3 | 27-54 hours | Medium-High | ✅ Yes (per template) | +| Phase 4 | 1-2 hours | Low | ✅ Yes | +| Phase 5 | 1-2 hours | Low | ❌ No (final cleanup) | +| **Total** | **36-69 hours** | | | + +**Recommended Schedule**: +- Week 1: Phases 0-2 (Foundation) - 7-11 hours +- Week 2-4: Phase 3 (Templates) - 5-10 templates per week +- Week 5: Phases 4-5 (Finalize) - 2-4 hours + +--- + +## Rollback Procedures + +### If Phase 0 or Phase 1 Fails + +```bash +# Revert TypeScript changes +git checkout web/src/ + +# Rebuild +npm run build:ts +go run . +``` + +### If Phase 2 Fails + +```bash +# Revert to Phase 1 state (window exports still present) +git checkout web/src/ + +# Rebuild +npm run build:ts +go run . +``` + +### If Phase 3 (Template Migration) Fails + +```bash +# Revert specific problematic template +git checkout templates/PROBLEM_TEMPLATE.templ + +# Regenerate templates +templ generate + +# Rebuild +go run . +``` + +All other migrated templates continue working. + +--- + +## Testing Checklist + +### After Each Phase + +- [ ] Build succeeds (`npm run build:ts`) +- [ ] Go build succeeds (`go build`) +- [ ] Application starts (`go run .`) +- [ ] Homepage loads +- [ ] Login works +- [ ] Dashboard loads +- [ ] No console errors +- [ ] Network tab shows no 404s for .js files + +### After Phase 1 (Internal Dependencies) + +- [ ] Dashboard works +- [ ] Library management works +- [ ] Collections work +- [ ] Admin functions work +- [ ] Queue works +- [ ] Conflicts work +- [ ] All toast notifications work +- [ ] All API calls work + +### After Phase 3 (Each Template) + +- [ ] Page loads +- [ ] All buttons work +- [ ] Modals open/close +- [ ] Dropdowns work +- [ ] Forms submit via HTMX +- [ ] Toast notifications appear +- [ ] No console errors +- [ ] Alpine DevTools shows reactive state (if installed) + +--- + +## Files Modified Summary + +### New Files Created +- None (alpine.ts already exists) + +### Source Files Modified (21 files) +- web/src/main.ts (verify imports are correct) +- web/src/alpine.ts (verify initialization is correct) +- web/src/api.ts (add ES exports, Alpine already present) +- web/src/toast.ts (add ES exports, Alpine already present) +- web/src/storage.ts (already has exports ✅) +- web/src/events.ts (add ES exports and Alpine) +- web/src/dom.ts (already has exports ✅) +- web/src/theme.ts (add ES exports and Alpine) +- web/src/header.ts (add ES exports and Alpine, restructure) +- web/src/woodPaneling.ts (add ES exports and Alpine) +- web/src/themeDropdown.ts (RESTRUCTURE to eliminate wrapping) +- web/src/library.ts (convert 55 window reads to imports, add Alpine) +- web/src/collections.ts (convert 54 window reads to imports, add Alpine) +- web/src/dashboard.ts (convert 10 window reads to imports, add Alpine) +- web/src/admin.ts (convert 14 window reads to imports, add Alpine) +- web/src/queue.ts (convert 8 window reads to imports, add Alpine) +- web/src/conflicts.ts (convert 6 window reads to imports, add Alpine) +- web/src/linking.ts (convert window reads to imports, add Alpine) +- web/src/custom-section-builder.ts (convert window reads to imports, add Alpine) +- web/src/analytics.ts (convert window reads to imports, add Alpine) +- web/src/bookshelf.ts (convert window reads to imports, add Alpine) +- web/src/api-explorer.ts (convert window reads to imports, add Alpine) +- web/src/device-management.ts (convert window reads to imports, add Alpine) +- web/src/search.ts (add Alpine registration) +- web/src/docs.ts (remove window globals, move inline JS to module, add Alpine) +- web/src/password_validation.ts (add Alpine registration) + +### Template Files Modified (27 files) +All templates updated to: +- Remove individual script tags +- Use single `` +- Replace `onclick` with `@click` +- Add `x-data` for stateful components (modals, dropdowns) +- Keep htmx.min.js separate + +### Generated Files +- web/static/main.js (bundled output with Alpine) +- web/static/main.js.map (sourcemap) + +### Files Deleted (Phase 5) +All individual .js files in web/static/ (except main.js, main.js.map, htmx.min.js, highlight.min.js, style.css) + +--- + +## External Dependencies (Not Bundled) + +### htmx.org +- **Status**: Keep as separate script tag +- **Reason**: Core framework, needs to load before main.js +- **Location**: `` + +### Chart.js +- **Status**: Keep on CDN +- **Reason**: 3.4MB minified, only used on analytics page +- **Location**: `` in analytics.templ only + +### highlight.js +- **Status**: Bundled via ESBuild +- **Reason**: Used in docs, small enough (~5KB gzipped) +- **Import**: `import hljs from "highlight.js";` + +### lunr +- **Status**: Bundled via ESBuild +- **Reason**: Used in docs search, small enough (~10KB gzipped) +- **Import**: `import * as lunr from "lunr";` + +--- + +## Architecture Decision Records + +### ADR-001: ES Modules over Window Globals + +**Decision**: Use ES module imports/exports for all TypeScript-to-TypeScript dependencies. + +**Rationale**: +- Standard JavaScript module system +- Better type safety with TypeScript +- Clear dependency chains +- Tree-shaking support +- No global namespace pollution + +**Consequences**: +- Positive: Cleaner code, better IDE support, easier refactoring +- Positive: Standard pattern, easier for new developers +- Neutral: Requires build step (already using ESBuild) + +### ADR-002: Alpine.js for Template Interactivity Only + +**Decision**: Use Alpine.js ONLY as a bridge between templates and TypeScript, not for internal TypeScript dependencies. + +**Rationale**: +- Alpine is designed for template directives (@click, x-show) +- Clean separation: ES modules for code, Alpine for templates +- Avoids over-engineering simple function calls +- Keeps bundle size smaller + +**Consequences**: +- Positive: Clean template syntax +- Positive: Progressive enhancement works +- Positive: Easy to understand data flow +- Neutral: Need to learn Alpine basics (simple) + +### ADR-003: SSR-First with Progressive Enhancement + +**Decision**: Server renders complete HTML with data, client-side JavaScript only for interactivity. + +**Rationale**: +- Faster initial page load +- Better SEO (if needed) +- Works without JavaScript (degrades gracefully) +- Simpler state management +- Aligns with HTMX philosophy + +**Consequences**: +- Positive: Better performance +- Positive: More resilient +- Positive: Easier to debug +- Neutral: Slightly more server work (acceptable) + +### ADR-004: Function Wrapping Elimination + +**Decision**: Eliminate function wrapping (themeDropdown.ts wraps header.ts functions) in favor of proper module composition. + +**Rationale**: +- Clearer code flow +- Better testability +- Easier to understand +- Standard pattern +- App not yet deployed, can refactor + +**Consequences**: +- Positive: Cleaner architecture +- Positive: Easier to maintain +- Negative: More work upfront (acceptable) +- Negative: Need to restructure (acceptable) + +--- + +## Troubleshooting + +### Build Errors + +**Error**: "Cannot find module './xxx'" + +**Solution**: +- Check import path is correct (relative, case-sensitive) +- Check file has `export {}` statements +- Run `npm run build:ts` with clean build + +**Error**: "Alpine is not defined" + +**Solution**: +- Check alpine.ts is imported in main.ts: `import "./alpine";` +- Check Alpine.start() is called +- Check window.Alpine is set + +**Error**: "Cannot read property 'xxx' of undefined" + +**Solution**: +- Check Alpine.global() is called after Alpine.start() +- Check namespace is correct (e.g., `api.post` not `window.api.post`) +- Check template uses correct namespace: `@click="api.post()"` + +### Runtime Errors + +**Error**: "@click handler not working" + +**Possible Causes**: +1. Alpine not loaded + - Check browser console: `window.Alpine` should be defined + - Check main.js is loaded + - Check alpine.ts imports Alpine and starts it + +2. Function not registered with Alpine + - Check source file has `Alpine.global("namespace", { ... })` + - Check namespace matches template usage + +3. Template syntax error + - Check @click syntax: `@click="namespace.function()"` + - Check for typos + +**Error**: "x-show not working" + +**Possible Causes**: +1. Missing x-data parent + - Add `x-data="{ varName: false }"` to parent element + +2. Variable name mismatch + - Check x-data variable name matches x-show variable + +3. Alpine not loaded + - See above + +### Template Errors + +**Error**: "templ generate fails" + +**Solution**: +- Check template syntax (missing closing tags, etc.) +- Check for invalid templ syntax +- Check template file encoding (UTF-8) + +**Error**: "Page not rendering correctly after migration" + +**Solution**: +- Check all script tags are removed except main.js +- Check main.js is loaded +- Check browser console for errors +- Check Alpine DevTools for state +- Verify onclick → @click conversion is correct + +### Performance Issues + +**Issue**: "Bundle size too large (>200KB)" + +**Possible Causes**: +- Check if Chart.js accidentally bundled (should be CDN) +- Check if duplicate dependencies +- Run `esbuild --analyze` to see bundle contents + +**Issue**: "Page load slow" + +**Possible Causes**: +- Check main.js is minified in production +- Check sourcemap not loaded in production +- Check server compression enabled +- Check browser caching headers + +--- + +## Development Workflow + +### During Migration (Phases 0-2) + +```bash +# Terminal 1: Watch TypeScript +npm run build:ts:watch + +# Terminal 2: Watch Templates +templ generate -watch + +# Terminal 3: Run Server +go run . +``` + +### After Migration (All Phases Complete) + +```bash +# Development +npm run dev + +# Production Build +npm run build +templ generate +go build +``` + +--- + +## FAQ + +### Q: Why not put everything in main.ts? + +**A**: Main.ts imports other modules. This keeps code: +- Organized (one file per concern) +- Maintainable (easy to find code) +- Testable (can test individual modules) +- Tree-shakeable (unused code eliminated) + +### Q: Why Alpine.js instead of vanilla JS event listeners? + +**A**: Alpine provides: +- Cleaner template syntax (@click vs onclick) +- Built-in state management (x-show, x-data) +- Better progressive enhancement +- SSR-friendly +- Smaller bundle than React/Vue + +### Q: Why keep HTMX if we have Alpine? + +**A**: They serve different purposes: +- **HTMX**: Server communication (form submissions, API calls) +- **Alpine**: Client-side state (modals, dropdowns, UI) + +They work great together. + +### Q: Can I use React/Vue instead of Alpine? + +**A**: You could, but: +- **Larger bundle size**: React = ~40KB gzipped, Alpine = ~15KB gzipped +- **More complexity**: Need JSX compilation, more build tools +- **Overkill**: For this app's needs, Alpine is sufficient +- **HTMX synergy**: Alpine works better with HTMX + +### Q: What if I need to add a new page? + +**A**: Follow the dashboard pattern: +1. Create Go handler that renders template with data +2. Create .templ file with SSR data +3. Use Alpine for any client-side interactivity +4. Import TypeScript modules in main.ts +5. Register functions with Alpine if templates call them + +### Q: How do I debug issues? + +**A**: +1. **Browser DevTools Console**: Check for errors +2. **Network Tab**: Check main.js loads, no 404s +3. **Alpine DevTools**: Install browser extension to inspect state +4. **Sourcemaps**: Use main.js.map to debug original TypeScript +5. **Go Logs**: Check server logs for errors + +--- + +## Glossary + +- **ES Modules**: Standard JavaScript module system (import/export) +- **ESBuild**: Fast JavaScript bundler +- **Alpine.js**: Lightweight JavaScript framework for UI interactivity +- **HTMX**: Library for dynamic web pages using HTML attributes +- **Templ**: Go templating language that compiles to Go code +- **SSR**: Server-Side Rendering - server generates complete HTML +- **Progressive Enhancement**: Page works without JavaScript, enhanced with it +- **Tree-shaking**: Removing unused code from bundle +- **Sourcemap**: File that maps bundled code back to source code for debugging +- **Window globals**: Variables attached to window object (old pattern) +- **Namespace**: Grouping related functions (e.g., api.post, api.get) + +--- + +## Appendix A: Quick Reference + +### Common Patterns + +**Import ES Module**: +```typescript +import { functionName } from "./module"; +``` + +**Export from Module**: +```typescript +export { functionName1, functionName2 }; +export default function mainFunction() { ... } +``` + +**Register with Alpine**: +```typescript +import { Alpine } from "./alpine"; + +Alpine.global("namespace", { + functionName1, + functionName2, +}); +``` + +**Use in Template**: +```html + + +
+ Modal content +
+ +``` + +--- + +## Appendix B: File-by-File Checklist + +### Phase 0: Add ES Exports + +- [ ] api.ts - Add exports +- [ ] toast.ts - Add exports +- [ ] storage.ts - Already has exports ✅ +- [ ] events.ts - Add exports +- [ ] dom.ts - Already has exports ✅ +- [ ] theme.ts - Add exports +- [ ] header.ts - Add exports +- [ ] woodPaneling.ts - Add exports + +### Phase 1: Internal Dependencies + +- [ ] dashboard.ts - Convert 10 window reads +- [ ] search.ts - No changes needed +- [ ] device-management.ts - Convert 4 window reads +- [ ] admin.ts - Convert 14 window reads +- [ ] queue.ts - Convert 8 window reads +- [ ] conflicts.ts - Convert 6 window reads +- [ ] linking.ts - Convert window reads +- [ ] custom-section-builder.ts - Convert window reads +- [ ] analytics.ts - Convert window reads +- [ ] bookshelf.ts - Convert window reads +- [ ] api-explorer.ts - Convert window reads +- [ ] library.ts - Convert 55 window reads +- [ ] collections.ts - Convert 54 window reads +- [ ] themeDropdown.ts - RESTRUCTURE to eliminate wrapping +- [ ] docs.ts - Remove window globals + +### Phase 2: Alpine Registration + +- [ ] api.ts - Already registered ✅ +- [ ] toast.ts - Already registered ✅ +- [ ] storage.ts - Add Alpine.global +- [ ] events.ts - Add Alpine.global +- [ ] dom.ts - Add Alpine.global +- [ ] theme.ts - Add Alpine.global +- [ ] header.ts - Add Alpine.global +- [ ] woodPaneling.ts - Add Alpine.global +- [ ] library.ts - Add Alpine.global +- [ ] collections.ts - Add Alpine.global +- [ ] admin.ts - Add Alpine.global +- [ ] queue.ts - Add Alpine.global +- [ ] conflicts.ts - Add Alpine.global +- [ ] docs.ts - Add Alpine.global +- [ ] search.ts - Add Alpine.global +- [ ] device-management.ts - Add Alpine.global +- [ ] password_validation.ts - Add Alpine.global + +### Phase 3: Template Migration + +- [ ] login.templ +- [ ] register.templ +- [ ] profile.templ +- [ ] settings.templ +- [ ] stats.templ +- [ ] sync.templ +- [ ] progress.templ +- [ ] custom_section.templ +- [ ] collection_rules.templ +- [ ] queue.templ +- [ ] conflicts.templ +- [ ] admin.templ +- [ ] admin_library.templ +- [ ] admin_users.templ +- [ ] dashboard.templ +- [ ] bookshelf.templ +- [ ] devices.templ +- [ ] analytics.templ +- [ ] api_explorer.templ +- [ ] collections.templ +- [ ] docs.templ +- [ ] header.templ +- [ ] collection_modal.templ +- [ ] restore_system_collection_modal.templ +- [ ] profile_modal.templ +- [ ] profile_form.templ +- [ ] toast.templ + +### Phase 4: Data Injection + +- [ ] Verify docs search still works +- [ ] Verify no data in window globals +- [ ] Document SSR-first pattern for future pages + +### Phase 5: Cleanup + +- [ ] Remove all old .js files +- [ ] Remove all window exports +- [ ] Update build scripts +- [ ] Final verification +- [ ] Update documentation + +--- + +## End of Migration Plan + +This plan provides a complete, incremental path from the current window-globals architecture to a modern ES modules + Alpine architecture, with clear phases, testing strategies, and rollback procedures. + +**Key Points**: +- Each phase is complete and testable +- Can ship after any phase (except final cleanup) +- No legacy code remains +- Clean architecture ready for launch +- SSR-first for future pages + +**Next Step**: Begin Phase 0 - Add ES module exports to utility modules. diff --git a/ESBUILD_README.md b/ESBUILD_README.md new file mode 100644 index 0000000..5c075e1 --- /dev/null +++ b/ESBUILD_README.md @@ -0,0 +1,210 @@ +# ESBuild Migration - Quick Reference + +## Status: Ready to Execute + +**Created**: Complete migration plan (ESBUILD_MIGRATION_PLAN.md) +**Archived**: Old incomplete plan (ESBUILD_SETUP_OLD.md) + +## The Problem + +- **274 window global references** across 21 TypeScript files +- **193+ internal TypeScript dependencies** using window instead of ES imports +- **27 template files** with 150+ unique onclick handlers +- **Function wrapping** creating tight coupling +- **Individual .js files** instead of single bundle + +## The Solution + +**Incremental migration** with no legacy code: + +1. **Phase 0**: Add ES exports (1-2 hours) - Can ship ✅ +2. **Phase 1**: Convert internal dependencies (4-6 hours) - Can ship ✅ +3. **Phase 2**: Alpine.js bridge (2-3 hours) - Can ship ✅ +4. **Phase 3**: Template migration (27-54 hours) - Can ship per template ✅ +5. **Phase 4**: Data injection (1-2 hours) - Can ship ✅ +6. **Phase 5**: Cleanup (1-2 hours) - Final step + +**Total**: 36-69 hours over ~5 weeks + +## Key Architecture Decisions + +### 1. ES Modules for TypeScript → TypeScript +```typescript +// library.ts +import { api } from "./api"; +import { showToast } from "./toast"; + +const response = await api.get("/libraries"); +showToast.success("Loaded!"); +``` + +### 2. Alpine.js for Templates → TypeScript ONLY +```typescript +// api.ts (bottom of file) +import { Alpine } from "./alpine"; + +Alpine.global("api", { + get: apiGet, + post: apiPost, + // ... +}); +``` + +```html + + +``` + +### 3. SSR-First with Progressive Enhancement +- Server renders complete HTML with data +- Client-side JavaScript only for interactivity +- Data loaded via fetch() APIs +- No window globals for data + +## File Changes + +### 21 TypeScript Files +All will have: +- ES module exports (`export { functions }`) +- ES module imports (`import { functions } from "./module"`) +- Alpine registration (`Alpine.global("namespace", { ... })`) +- NO window exports (cleaned up in Phase 5) + +### 27 Template Files +All will have: +- Single script tag: `` +- Alpine directives: `@click` instead of `onclick` +- Alpine state: `x-data` for modals/dropdowns +- NO individual .js file loads + +## Quick Start + +### Right Now: Start Phase 0 + +```bash +# 1. Open web/src/api.ts +# 2. Add at bottom (after Alpine.global block): +export { apiGet, apiPost, apiPut, apiDelete, apiPatch, handleResponse, handleVoidResponse, handleError }; + +# 3. Repeat for other utility modules (toast.ts, events.ts, theme.ts, header.ts, woodPaneling.ts) + +# 4. Build and test +npm run build:ts +go run . +``` + +### After Phase 0 Complete + +Move to Phase 1: Convert internal dependencies (see full plan) + +## Critical Path + +**Must complete in order**: +1. Phase 0 (ES exports) - Enables Phase 1 +2. Phase 1 (Internal imports) - Enables Phase 2 +3. Phase 2 (Alpine registration) - Enables Phase 3 +4. Phase 3 (Template migration) - Can do incrementally +5. Phase 4 (Data injection) - Verify pattern +6. Phase 5 (Cleanup) - Final polish + +## Safety Features + +✅ **Incremental**: Each phase is complete and testable +✅ **Ship anytime**: Can deploy after Phases 0-2, or during Phase 3 +✅ **Easy rollback**: Revert individual files if issues +✅ **No legacy**: Each file fully migrated, no half-states +✅ **Testing**: Clear verification criteria for each phase + +## Common Patterns + +### Before Migration +```typescript +// TypeScript +function doWork() { ... } +(window as any).doWork = doWork; + +// Another file +const result = (window as any).doWork(); +``` + +```html + + +``` + +### After Migration +```typescript +// work.ts +export function doWork() { ... } +import { Alpine } from "./alpine"; +Alpine.global("work", { doWork }); + +// Another file +import { doWork } from "./work"; +const result = doWork(); +``` + +```html + + +``` + +## Testing + +### After Each Phase +```bash +# Build +npm run build:ts + +# Run +go run . + +# Test +# - Homepage loads +# - Login works +# - Dashboard loads +# - No console errors +# - No 404s for .js files +``` + +## Rollback + +If any phase has issues: +```bash +# Revert changes +git checkout web/src/ # For TypeScript issues +git checkout templates/PROBLEM.templ # For template issues + +# Rebuild +npm run build:ts +templ generate +go run . +``` + +## Next Steps + +1. **Read** the full plan: `ESBUILD_MIGRATION_PLAN.md` +2. **Start** Phase 0: Add ES exports (1-2 hours) +3. **Test** thoroughly after each phase +4. **Track** progress using checklist in plan +5. **Ask** questions if anything is unclear + +## Resources + +- **Full Plan**: ESBUILD_MIGRATION_PLAN.md (this document) +- **Old Plan**: ESBUILD_SETUP_OLD.md (archived, incomplete) +- **Alpine Docs**: https://alpinejs.dev/ +- **ESBuild Docs**: https://esbuild.github.io/ +- **Templ Docs**: https://github.com/a-h/templ + +## Support + +If you encounter issues: +1. Check the "Troubleshooting" section in the full plan +2. Review the "Testing Checklist" for your phase +3. Use the "Rollback Procedures" if needed +4. Reference the "Quick Reference" patterns + +--- + +**Remember**: This is an incremental migration with clear phases. Take it one phase at a time, test thoroughly, and you'll have a clean, modern codebase ready for launch. diff --git a/ESBUILD_SETUP_OLD.md b/ESBUILD_SETUP_OLD.md new file mode 100644 index 0000000..e0e4f61 --- /dev/null +++ b/ESBUILD_SETUP_OLD.md @@ -0,0 +1,2199 @@ +# 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: +- Essentials guide: +- `x-data` docs: +- `@click` docs: +- `x-show` docs: +- `x-transition` docs: + +### Alpine + HTMX Integration + +- Blog post: +- Both work together seamlessly - Alpine for client state, HTMX for server calls + +### ESBuild Documentation + +- Official docs: +- API: +- Bundling: + +### 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 `