# 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 This section eliminates the anti-pattern of wrapping functions from other files. Currently, `themeDropdown.ts` overrides `header.ts` functions by reading from window and reassigning them. We'll convert this to proper ES module composition. --- #### 1.4.1: web/src/themeDropdown.ts - Complete Rewrite **Current Problem (Lines 1-32):** ```typescript // Anti-pattern: Reading and reassigning window globals const originalToggleThemeDropdown = (window as any).toggleThemeDropdown; (window as any).toggleThemeDropdown = () => { originalToggleThemeDropdown(); updateThemeIndicators(); (window as any).updateWoodPanelingIndicators?.(); }; ``` **Step-by-Step Conversion:** **1. Add ES Module Imports (at top of file, after line 1):** ```typescript import { toggleThemeDropdown } from "./header"; import { updateWoodPanelingIndicators } from "./woodPaneling"; ``` **2. Remove window read/override (delete lines 1-8):** ```typescript // DELETE THESE LINES: // const originalToggleThemeDropdown = (window as any).toggleThemeDropdown; // (window as any).toggleThemeDropdown = () => { // originalToggleThemeDropdown(); // updateThemeIndicators(); // (window as any).updateWoodPanelingIndicators?.(); // }; ``` **3. Create new composed function (replace deleted code with):** ```typescript // Compose the theme dropdown behavior export function initializeThemeDropdown(): void { // Call original function from header.ts toggleThemeDropdown(); // Then update theme indicators updateThemeIndicators(); // Update wood paneling indicators updateWoodPanelingIndicators(); } ``` **4. Keep existing updateThemeIndicators function (no change needed)** **5. Add Alpine registration at bottom of file (after existing code):** ```typescript import { Alpine } from "./alpine"; // Register composed function with Alpine Alpine.global("themeDropdown", { initialize: initializeThemeDropdown, updateIndicators: updateThemeIndicators, }); // Also keep the window export temporarily (remove in Phase 2) (window as any).initializeThemeDropdown = initializeThemeDropdown; (window as any).updateThemeIndicators = updateThemeIndicators; ``` **Complete File After Conversion:** ```typescript // web/src/themeDropdown.ts - Rewritten version import { toggleThemeDropdown } from "./header"; import { updateWoodPanelingIndicators } from "./woodPaneling"; import { Alpine } from "./alpine"; // Composed theme dropdown initialization export function initializeThemeDropdown(): void { // Call original function from header.ts toggleThemeDropdown(); // Then update theme indicators updateThemeIndicators(); // Update wood paneling indicators updateWoodPanelingIndicators(); } // Update theme indicator elements (existing function, keep as-is) export function updateThemeIndicators(): void { const themeSelect = document.getElementById("theme-select") as HTMLSelectElement; const themeIndicator = document.getElementById("current-theme"); if (themeSelect && themeIndicator) { const theme = themeSelect.value as ThemeType; themeIndicator.textContent = theme.charAt(0).toUpperCase() + theme.slice(1); } // Update wood paneling button const woodPanelingButton = document.getElementById("wood-paneling-button"); if (woodPanelingButton) { const woodPaneling = localStorage.getItem("woodPaneling") || "none"; woodPanelingButton.textContent = woodPaneling === "none" ? "🪵" : "✓"; } } // Register with Alpine for template access Alpine.global("themeDropdown", { initialize: initializeThemeDropdown, updateIndicators: updateThemeIndicators, }); // Temporary window exports (remove in Phase 2) (window as any).initializeThemeDropdown = initializeThemeDropdown; (window as any).updateThemeIndicators = updateThemeIndicators; ``` **Changes Summary:** - ✅ Added 3 ES imports (toggleThemeDropdown, updateWoodPanelingIndicators, Alpine) - ✅ Deleted 8 lines of window read/override code - ✅ Created 1 new composed function (initializeThemeDropdown) - ✅ Added Alpine.global() registration (7 lines) - ✅ Kept window exports temporarily (2 lines, remove in Phase 2) --- #### 1.4.2: web/src/docs.ts - Window Export Conversion Only **⚠️ IMPORTANT:** Search functionality changes (Lunr removal, backend API) are handled by a **separate DOCS_SEARCH_IMPLEMENTATION.md task**. This section ONLY covers ESBuild migration changes (window export → Alpine.global()). --- **Current Issue (Line 91):** ```typescript (window as any).toggleSidebar = toggleSidebar; ``` This is the only window global that needs to be converted for the ESBuild migration. **Note:** The search functionality (`performDocsSearch`, `initializeDocsSearch`, Lunr imports, etc.) will be addressed separately by the backend API search implementation (see DOCS_SEARCH_IMPLEMENTATION.md). --- **Step-by-Step Conversion:** **1. Add Alpine Import (after line 1):** ```typescript // BEFORE: import * as lunr from "lunr"; // AFTER: import * as lunr from "lunr"; import { Alpine } from "./alpine"; ``` **2. Remove Window Export and Add Alpine Registration (replace line 91):** ```typescript // DELETE line 91: // (window as any).toggleSidebar = toggleSidebar; // REPLACE with: // Register with Alpine for template access Alpine.global("docs", { toggleSidebar, initializeSearch: initializeDocsSearch, }); ``` **3. Add ES Module Export (at end of file):** ```typescript export { toggleSidebar, initializeDocsSearch }; ``` --- **Complete Converted File (ESBuild changes only):** ```typescript // web/src/docs.ts - ESBuild migration changes import * as lunr from "lunr"; import { Alpine } from "./alpine"; function toggleSidebar(): void { const sidebar = document.getElementById("docs-sidebar"); const overlay = document.getElementById("docs-overlay"); if (sidebar && overlay) { sidebar.classList.toggle("translate-x-0"); sidebar.classList.toggle("-translate-x-full"); overlay.classList.toggle("hidden"); } } function initializeDocsSearch(): void { const searchInput = document.getElementById( "docs-search", ) as HTMLInputElement; const searchResults = document.getElementById("docs-search-results"); if (!searchInput || !searchResults) return; let docsSearchTimeout: ReturnType | null = null; searchInput.addEventListener("input", () => { const query = searchInput.value.trim(); if (docsSearchTimeout) { clearTimeout(docsSearchTimeout); } if (query.length < 2) { searchResults.innerHTML = ""; searchResults.classList.add("hidden"); return; } docsSearchTimeout = setTimeout(() => { performDocsSearch(query); }, 300); }); } function performDocsSearch(query: string): void { const searchResults = document.getElementById("docs-search-results"); if (!searchResults) return; try { const idx = lunr.Index.load(lunrIndexData); if (!idx) { searchResults.innerHTML = '

Search index not loaded

'; searchResults.classList.remove("hidden"); return; } const results = idx.search(query); if (results.length === 0) { searchResults.innerHTML = '

No results found

'; } else { searchResults.innerHTML = results .slice(0, 10) .map((result: { ref: string }) => { const doc = docsData[result.ref]; if (!doc) return ""; return `

${doc.title || result.ref}

${doc.section ? `

${doc.section}

` : ""}
`; }) .join(""); } searchResults.classList.remove("hidden"); } catch (error) { console.error("Search error:", error); searchResults.innerHTML = '

Search error

'; searchResults.classList.remove("hidden"); } } document.addEventListener("DOMContentLoaded", () => { initializeDocsSearch(); }); // Register with Alpine for template access Alpine.global("docs", { toggleSidebar, initializeSearch: initializeDocsSearch, }); // ES module exports export { toggleSidebar, initializeDocsSearch }; ``` --- **ESBuild Migration Changes Only:** - ✅ Added 1 import (Alpine from "./alpine") - ✅ Removed 1 line (window export) - ✅ Added 7 lines (Alpine.global() registration) - ✅ Added 1 line (ES module export) **Search Functionality:** Unchanged (handled by separate DOCS_SEARCH_IMPLEMENTATION.md task) **Note:** The `lunrIndexData` and `docsData` undefined variables will be fixed when the backend API search is implemented (see DOCS_SEARCH_IMPLEMENTATION.md). --- #### 1.4.3: No Other Files Need Function Wrapping Changes **Confirmed Analysis:** - ✅ `header.ts` - Only exports functions, no wrapping - ✅ `woodPaneling.ts` - Only exports functions, no wrapping - ✅ All other consumer files - Only import/use functions, no wrapping **Only 2 files affected:** themeDropdown.ts and docs.ts --- #### 1.4.4: Verification Steps After completing both file conversions: ```bash # Build TypeScript npm run build:ts # Run application go run . # Test in browser: # 1. Navigate to dashboard # 2. Click theme dropdown - should work and update indicators # 3. Navigate to /docs # 4. Search in docs - should work without console errors ``` **Expected Results:** - ✅ Theme dropdown opens and updates indicators - ✅ Wood paneling indicators update correctly - ✅ Docs search works (lunr loaded via ES import) - ✅ No console errors about missing window.lunr - ✅ No console errors about missing window.lunrIndex - ✅ No console errors about missing window.docsData **Common Issues:** - If theme dropdown doesn't open: Check if toggleThemeDropdown import is correct - If indicators don't update: Check if updateWoodPanelingIndicators is called - If docs search fails: Check if lunr is imported correctly (not via window) - If data missing: Check if fetch() API is working (not using window globals) ### Step 1.5: Remove Window Exports from Consumer Files (If Present) After converting all reads to imports, check if consumer files have window exports to remove. **Note:** Not all files have window exports. Some files (like dashboard.ts) use event delegation and don't export to window. **Files WITH window exports to remove:** **web/src/library.ts** (lines 690-704) ```typescript // DELETE these window exports: (window as any).deleteLibrary = deleteLibrary; (window as any).showLibraryFolders = showLibraryFolders; (window as any).addLibraryFolder = addLibraryFolder; (window as any).removeLibraryFolder = removeLibraryFolder; (window as any).setLibraryVisibility = setLibraryVisibility; (window as any).loadUserVisibility = loadUserVisibility; (window as any).editLibrary = editLibrary; (window as any).handleCreateLibrarySubmit = handleCreateLibrarySubmit; (window as any).showFolderBrowser = showFolderBrowser; (window as any).navigateFolderBrowser = navigateFolderBrowser; (window as any).selectBrowseFolder = selectBrowseFolder; (window as any).hideFolderBrowser = hideFolderBrowser; (window as any).showDeleteModal = showDeleteModal; (window as any).hideDeleteModal = hideDeleteModal; (window as any).confirmDeleteLibrary = confirmDeleteLibrary; ``` **web/src/collections.ts** (multiple locations: lines 204-208, 243, 315-317, 472-476, 912-923) ```typescript // DELETE all window exports (26 functions total) // Examples: (window as any).loadCollections = loadCollections; (window as any).loadCollectionRules = loadCollectionRules; // ... etc (all 26 exports) ``` **web/src/bookshelf.ts** (lines 127-130) ```typescript // DELETE these window exports: (window as any).selectLibrary = selectLibrary; (window as any).loadBookshelf = loadBookshelf; (window as any).selectBook = selectBook; (window as any).changePage = changePage; ``` **web/src/api-explorer.ts** (lines 186-189) ```typescript // DELETE these window exports: (window as any).sendApiRequest = sendApiRequest; (window as any).loadFromHistory = loadFromHistory; (window as any).copyCurl = copyCurl; (window as any).formatJson = formatJson; ``` **Files WITHOUT window exports (already clean):** - ✅ **dashboard.ts** - Uses event delegation, no window exports - ✅ **admin.ts** - Check if it has exports - ✅ **queue.ts** - Check if it has exports - ✅ **conflicts.ts** - Check if it has exports - ✅ **analytics.ts** - Check if it has exports - ✅ **device-management.ts** - Check if it has exports - ✅ **linking.ts** - Check if it has exports - ✅ **custom-section-builder.ts** - Check if it has exports **Verification:** For each file, search for `(window as any).` at the end of the file. If present, remove those lines. **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.