Files
bookhoard/ESBUILD_MIGRATION_PLAN.md
T
john-okeefe c0290bb106 docs: Update ESBuild migration plan documentation
Updated ESBUILD_MIGRATION_PLAN.md to reflect:
- Phase 1 completion status
- New Alpine.js conversion work
- Remaining templates status (docs.templ excluded per user request)
2026-03-08 21:36:40 -04:00

2041 lines
57 KiB
Markdown

# 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<Response> { ... }
export async function apiPost(url: string, data?: unknown): Promise<Response> { ... }
export async function handleResponse<T>(response: Response): Promise<T> { ... }
```
```typescript
// library.ts (consumes api.ts)
import { apiGet, handleResponse } from "./api";
const response = await apiGet("/libraries");
const result = await handleResponse<LibrariesResponse>(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
<!-- Before -->
<button onclick="apiPost('/api/save', data)">Save</button>
<!-- After -->
<button @click="api.post('/api/save', data)">Save</button>
```
### 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<typeof setTimeout> | 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 =
'<p class="p-2 text-sm" style="color: var(--text-secondary)">Search index not loaded</p>';
searchResults.classList.remove("hidden");
return;
}
const results = idx.search(query);
if (results.length === 0) {
searchResults.innerHTML =
'<p class="p-2 text-sm" style="color: var(--text-secondary)">No results found</p>';
} else {
searchResults.innerHTML = results
.slice(0, 10)
.map((result: { ref: string }) => {
const doc = docsData[result.ref];
if (!doc) return "";
return `
<a href="${result.ref}" class="block p-2 hover:bg-opacity-50 transition-colors" style="background-color: var(--bg-secondary)">
<p class="font-medium text-sm" style="color: var(--text-primary)">${doc.title || result.ref}</p>
${doc.section ? `<p class="text-xs" style="color: var(--text-secondary)">${doc.section}</p>` : ""}
</a>
`;
})
.join("");
}
searchResults.classList.remove("hidden");
} catch (error) {
console.error("Search error:", error);
searchResults.innerHTML =
'<p class="p-2 text-sm" style="color: var(--text-secondary)">Search error</p>';
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
<script src="/static/htmx.min.js"></script>
<script src="/static/toast.js"></script>
<script src="/static/api.js"></script>
<script src="/static/events.js"></script>
<script src="/static/dom.js"></script>
<script src="/static/admin.js"></script>
```
**After**:
```templ
<script src="/static/htmx.min.js"></script>
<script src="/static/main.js"></script>
```
**Note**: Keep htmx.min.js separate (loaded before main.js)
#### Step B: Convert onclick to @click
**Before**:
```templ
<button onclick="toggleThemeDropdown()">Theme</button>
<div id="theme-dropdown" class="hidden">
<button onclick="changeThemeTo('tokyo-night')">Tokyo Night</button>
</div>
```
**After**:
```templ
<button @click="header.toggleThemeDropdown()">Theme</button>
<div x-show="themeDropdownOpen" @click.outside="themeDropdownOpen = false" x-transition>
<button @click="header.changeThemeTo('tokyo-night')">Tokyo Night</button>
</div>
```
#### Step C: Add Alpine State for UI Components
For modals, dropdowns, and any UI with show/hide state:
**Before**:
```templ
<button onclick="showAddBooksModal()">Add Books</button>
<div id="add-books-modal" class="hidden">
...modal content...
<button onclick="hideAddBooksModal()">Cancel</button>
</div>
```
**After**:
```templ
<div x-data="{ addBooksModalOpen: false }">
<button @click="addBooksModalOpen = true">Add Books</button>
<div x-show="addBooksModalOpen"
x-transition
@click.self="addBooksModalOpen = false"
class="fixed inset-0 ...">
...modal content...
<button @click="addBooksModalOpen = false">Cancel</button>
</div>
</div>
```
**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
<button onclick="showToast.success('Saved!')">Save</button>
<button onclick="apiPost('/api/save', data)">Save</button>
```
**After**:
```templ
<button @click="showToast.success('Saved!')">Save</button>
<button @click="api.post('/api/save', data)">Save</button>
```
### 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
<!-- templates/docs.templ -->
<script>
document.addEventListener('DOMContentLoaded', () => {
window.Alpine.docs.initializePage();
});
</script>
```
#### header.templ - Most Critical (Used in 17 Places)
This template is included in 17 other templates. Test thoroughly:
```templ
<!-- Before -->
<button onclick="toggleThemeDropdown()">Theme</button>
<div id="theme-dropdown" class="hidden">
<button onclick="changeThemeTo('tokyo-night')">Tokyo Night</button>
...11 more themes...
<button onclick="changeWoodPaneling('none')">None</button>
<button onclick="changeWoodPaneling('wood-light')">Wood Light</button>
</div>
<button onclick="toggleUserMenu()">
<div id="user-menu" class="hidden">
<button onclick="logout()">Logout</button>
</div>
```
```templ
<!-- After -->
<div x-data="{ themeDropdownOpen: false, userMenuOpen: false }">
<button @click="themeDropdownOpen = !themeDropdownOpen">Theme</button>
<div x-show="themeDropdownOpen"
@click.outside="themeDropdownOpen = false"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
class="absolute right-0 mt-2 w-64 rounded-lg shadow-lg z-50">
<button @click="header.changeThemeTo('tokyo-night'); themeDropdownOpen = false">Tokyo Night</button>
...11 more themes...
<button @click="woodPaneling.change('none'); themeDropdownOpen = false">None</button>
<button @click="woodPaneling.change('wood-light'); themeDropdownOpen = false">Wood Light</button>
</div>
<button @click="userMenuOpen = !userMenuOpen">
<div x-show="userMenuOpen"
@click.outside="userMenuOpen = false"
x-transition>
<button @click="header.logout(); userMenuOpen = false">Logout</button>
</div>
</div>
```
### 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 `<script src="/static/main.js"></script>`
- 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**: `<script src="/static/htmx.min.js"></script>`
### Chart.js
- **Status**: Keep on CDN
- **Reason**: 3.4MB minified, only used on analytics page
- **Location**: `<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>` 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
<!-- Before -->
<button onclick="namespace.functionName()">
<!-- After -->
<button @click="namespace.functionName()">
```
**Add State with Alpine**:
```html
<div x-data="{ modalOpen: false }">
<button @click="modalOpen = true">Open</button>
<div x-show="modalOpen" x-transition>
Modal content
</div>
</div>
```
---
## 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.