Comprehensive update to esbuild-setup.md with Alpine.js integration guide for replacing (window as any) pattern with modern reactive framework. Key changes: - Add Alpine.js as recommended approach over vanilla event listeners - Include Phase 2: TypeScript Alpine registration patterns - Update Phase 3: Template changes with @click and x-data examples - Add Phase 5: Step-by-step TypeScript migration with exact line numbers - Fix toast.ts, api.ts, storage.ts examples to match actual code structure - Include troubleshooting for Alpine-specific issues - Add migration checklist and quick reference guide Architecture decisions: - Alpine.js for client-side state (modals, dropdowns, theme) - HTMX for server calls (existing pattern, keep unchanged) - Hybrid approach: Alpine reactive components + HTMX forms - Bundle Alpine with ESBuild (~15KB gzipped) Template updates (27 files): - Replace onclick="func()" with @click="func()" - Add x-data for stateful components - Use x-show/x-transition for modals and dropdowns - Keep HTMX form submissions unchanged TypeScript migrations: - Priority 1: Core utilities (toast.ts, api.ts, storage.ts, events.ts, dom.ts) - Priority 2: Stateful components (header.ts themeDropdown, woodPaneling.ts) - Priority 3: Page-specific functions (collections.ts, devices.ts, etc.) - Register functions with Alpine.global() or Alpine.data() Testing and verification: - Alpine DevTools for debugging reactive state - Build step: esbuild --run scripts/build-docs-search.ts - Verify no 404 errors for missing .js files - Test all 151 onclick handlers work with @click Bundle size: ~130KB minified (~40KB gzipped) with Alpine included Browser support: ES2020 (Chrome 80+, Firefox 72+, Safari 13.1+) Document is now 1,812 lines with comprehensive step-by-step instructions for migrating from (window as any) exports to Alpine.js components.
50 KiB
ESBuild Setup Guide
Overview
Migrate to a single bundled main.js using ESBuild for better performance, simplified deployment, and broader browser compatibility (ES2020 target = Chrome 80+, Firefox 72+, Safari 13.1+, Edge 80+).
Target Bundle Size: ~100KB minified + gzip
Browser Support: ES2020 (Chrome 80+, Firefox 72+, Safari 13.1+, Edge 80+, Ubuntu 22.04 LTS browsers)
Architecture: SSR-first, procedural TypeScript, progressive enhancement (per PROJECT_GUIDELINES.md)
Status: What's Already Done ✅
1. package.json Dependencies (Already Correct)
File: package.json
Status: ✅ Already configured - NO CHANGES NEEDED
Dependencies (lines 14-26):
htmx.org,alpinejs,lunr,highlight.jsalready in dependenciesesbuild,typescript, TailwindCSS tooling already in devDependencies
2. Build Scripts (Already Correct)
File: package.json
Status: ✅ Already configured - NO CHANGES NEEDED
Lines 8-10:
"build:ts": "esbuild web/src/main.ts --bundle --outfile=web/static/main.js --sourcemap --target=es2020 --minify",
"watch:ts": "esbuild web/src/main.ts --bundle --outfile=web/static/main.js --sourcemap --target=es2020",
"dev": "concurrently \"npm run watch:css\" \"npm run watch:ts\" \"npm run watch:templ\"",
Already using --target=es2020 for broad browser compatibility.
3. Entry Point (Already Exists)
File: web/src/main.ts
Status: ✅ Already exists - NO CHANGES NEEDED
Already imports all 29 modules correctly (24 lines). Main.ts stays simple - imports happen in individual files where used.
Phase 1: Clean Up docs.ts
File: web/src/docs.ts (99 lines)
Status: Imports already added ✅
Lines to modify: 50, 56, 73 (remove window globals), 99 (replace with Alpine)
Note: Lines 3-4 already have the correct imports:
import * as lunr from "lunr";
import hljs from "highlight.js";
Step 1: Remove (window as any).lunr check
Location: Line 50
Current (lines 49-53):
if (!(window as any).lunr) {
console.warn("Lunr.js not loaded");
return;
}
Change to:
// Lunr now bundled via ESBuild
Step 2: Replace (window as any).lunrIndex with direct lunr usage
Location: Line 56
Current (lines 55-62):
const idx = (window as any).lunrIndex;
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;
}
Change to:
const idx = lunr.Builder.loadJs(searchIndex);
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;
}
Step 3: Replace (window as any).docsData with direct import
Location: Line 73
Current (lines 70-76):
.map((result: { ref: string }) => {
const doc = (window as any).docsData?.[result.ref];
if (!doc) return "";
Change to:
.map((result: { ref: string }) => {
const doc = docs[result.ref];
if (!doc) return "";
Step 4: Replace window export with Alpine global
Location: Line 99
Current (lines 98-99):
document.addEventListener("DOMContentLoaded", () => {
initializeDocsSearch();
});
(window as any).toggleSidebar = toggleSidebar;
Change to:
document.addEventListener("DOMContentLoaded", () => {
initializeDocsSearch();
// Register with Alpine globally
if (typeof window.Alpine !== 'undefined') {
window.Alpine.effect(() => {
window.Alpine.global('docs', {
toggleSidebar
});
});
}
});
Why Alpine.global(): Makes toggleSidebar() available to Alpine templates via @click="docs.toggleSidebar()"
Phase 2: Migrate TypeScript Files to Alpine Registration
Approach: Replace (window as any) exports with Alpine.js global registration
Architecture Note: Alpine.js for Client-Side State
Why Alpine over window exports:
- Modern, reactive framework (already in package.json)
- Clean template syntax:
@clickinstead ofonclick="window.func()" - Built-in state management:
x-data,x-show,x-model - Works with SSR (progressive enhancement)
- No global namespace pollution
Hybrid approach:
- Alpine: Client-side state (modals, dropdowns, theme, forms)
- HTMX: Server calls (already using for form submissions)
Step 1: Create Alpine Registration Helper
New file: web/src/alpine.ts
import Alpine from 'alpinejs';
// Initialize Alpine
window.Alpine = Alpine;
Alpine.start();
// Re-export Alpine for other modules to use
export { Alpine };
Add to main.ts: Append this line at the end of web/src/main.ts:
import './alpine';
Step 2: Update TypeScript Files to Register with Alpine
Pattern: Object Registration (Multiple Related Functions)
Example: web/src/toast.ts
Current (lines 229-236):
(window as any).showToast = {
error: (message: string, duration?: number) =>
showToast(message, "error", duration),
success: (message: string, duration?: number) =>
showToast(message, "success", duration),
info: (message: string, duration?: number) =>
showToast(message, "info", duration),
};
Change to:
import { Alpine } from './alpine';
// Register toast API with Alpine
Alpine.global('showToast', {
error: (message: string, duration?: number) =>
showToast(message, "error", duration),
success: (message: string, duration?: number) =>
showToast(message, "success", duration),
info: (message: string, duration?: number) =>
showToast(message, "info", duration),
});
Usage in templates:
<!-- Before -->
<button onclick="showToast.success('Saved!')">Save</button>
<button onclick="showToast.error('Failed!')">Retry</button>
<!-- After -->
<button @click="showToast.success('Saved!')">Save</button>
<button @click="showToast.error('Failed!')">Retry</button>
Pattern: Namespace Registration (Grouping Related Functions)
Example: web/src/api.ts
Current (lines 90-99):
(window as any).api = {
get: apiGet,
post: apiPost,
put: apiPut,
delete: apiDelete,
patch: apiPatch,
handleResponse,
handleVoidResponse,
handleError,
};
Change to:
import { Alpine } from './alpine';
Alpine.global('api', {
get: apiGet,
post: apiPost,
put: apiPut,
delete: apiDelete,
patch: apiPatch,
handleResponse,
handleVoidResponse,
handleError,
});
Usage in templates:
<!-- Before -->
<button onclick="api.post('/api/save', data)">Save</button>
<!-- After -->
<button @click="api.post('/api/save', data)">Save</button>
Pattern: Stateful Components (Dropdowns, Modals)
Example: web/src/header.ts (theme dropdown)
Current approach: Multiple window exports
New approach: Create Alpine component with state
Add to web/src/header.ts:
import { Alpine } from './alpine';
// Register theme dropdown component
Alpine.data('themeDropdown', () => ({
open: false,
toggle() {
this.open = !this.open;
},
changeTheme(theme: string) {
changeThemeTo(theme); // Reuse existing function
this.open = false;
},
init() {
// Load saved theme on init
applyTheme(loadTheme());
}
}));
Usage in templates:
<!-- Before -->
<button onclick="toggleThemeDropdown()">Theme</button>
<div id="theme-dropdown" class="hidden">
<button onclick="changeThemeTo('tokyo-night')">Tokyo Night</button>
</div>
<!-- After -->
<div x-data="themeDropdown">
<button @click="toggle()">Theme</button>
<div x-show="open" @click.away="open = false" x-transition>
<button @click="changeTheme('tokyo-night')">Tokyo Night</button>
</div>
</div>
Files Requiring Alpine Migration
High priority (used by 10+ templates):
web/src/toast.ts- RegistershowToastweb/src/api.ts- Registerapiobjectweb/src/events.ts- Register event functionsweb/src/storage.ts- Register storage helpers
Medium priority (stateful components):
5. web/src/header.ts - Create themeDropdown component
6. web/src/woodPaneling.ts - Create woodPaneling component
7. web/src/collections.ts - Register collection functions
8. web/src/conflicts.ts - Register conflict functions
9. web/src/search.ts - Register search functions
10. web/src/dom.ts - Register DOM helpers
Lower priority (page-specific):
11. web/src/dashboard.ts - Dashboard-specific functions
12. web/src/devices.ts - Device management functions
13. web/src/queue.ts - Queue management functions
14. web/src/analytics.ts - Analytics functions (Chart.js)
15. web/src/admin.ts - Admin functions
Keep as-is for now (already working):
- Lunr/Highlight.js imports (docs.ts) - already handled
- Chart.js usage - already loaded via CDN
Phase 3: Update Templates to Alpine Directives
Template Migration Strategy
151 onclick handlers need migration across 27 templates. Use this approach:
- Add
x-datacomponent to sections with state - Replace
onclickwith@click - Replace
class="hidden"withx-show="!open" - Add transitions with
x-transition - Use
x-modelfor form inputs
Template Changes (27 files)
1. templates/collections.templ
File: templates/collections.templ (271 lines)
Remove script blocks (lines 12-13, 111-112):
DELETE all individual <script> tags.
Replace final script (line 269):
<script src="/static/collections.js"></script>
Change to:
<script src="/static/main.js"></script>
Update onclick handlers:
Line 63 - Collection card:
<!-- Before -->
<div onclick="navigateToCollection(this)" data-href={...}>
<!-- After -->
<div @click="collections.navigate(this)" data-href={...} x-data>
Line 119 - Back button:
<!-- Before -->
<button onclick="backToCollections()" class="btn-secondary">
<!-- After -->
<button @click="collections.back()" class="btn-secondary">
Line 156 - Show add books modal:
<!-- Before -->
<button onclick="showAddBooksModal()" class="btn-primary">
<!-- After -->
<button @click="collections.showAddModal()" class="btn-primary">
Lines 231, 252 - Hide modal:
<!-- Before -->
<button onclick="hideAddBooksModal()">
<!-- After -->
<button @click="collections.hideAddModal()">
Line 255 - Add selected books:
<!-- Before -->
<button type="button" onclick="addSelectedBooks()">
<!-- After -->
<button type="button" @click="collections.addSelected()">
2. templates/docs.templ
File: templates/docs.templ (518 lines)
Remove external scripts (lines 16-18):
<script src="https://cdn.jsdelivr.net/npm/lunr@2.3.9/lunr.min.js"></script>
<script src="/static/lunr-flex.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
DELETE these lines entirely.
Note: lunr-flex.min.js doesn't exist - this causes 404 errors.
Replace docs.js script (line 517):
<script src="/static/docs.js"></script>
Change to:
<script src="/static/main.js"></script>
Update sidebar toggle (line 23-27):
<!-- Before -->
<button
class="lg:hidden fixed top-4 left-4 z-50 bg-background-secondary border border-border rounded p-2"
onclick="toggleSidebar()"
aria-label="Toggle menu">
Change to:
<!-- After -->
<div x-data="{ sidebarOpen: false }">
<button
class="lg:hidden fixed top-4 left-4 z-50 bg-background-secondary border border-border rounded p-2"
@click="sidebarOpen = !sidebarOpen"
aria-label="Toggle menu">
Add Alpine state to sidebar (line 37):
<!-- Before -->
<div class="sidebar fixed left-0 top-0 bottom-0 w-72 bg-background-secondary ...">
Change to:
<!-- After -->
<div class="sidebar fixed left-0 top-0 bottom-0 w-72 bg-background-secondary ..."
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full'"
x-show="sidebarOpen"
x-transition.opacity>
3. templates/admin.templ
File: templates/admin.templ (125 lines)
Replace script block (lines 9-11):
<script src="/static/api.js"></script>
<script src="/static/toast.js"></script>
<script src="/static/admin.js"></script>
Change to:
<script src="/static/main.js"></script>
4. templates/admin_library.templ
File: templates/admin_library.templ (163 lines)
Replace script block (lines 156-160):
<script src="/static/api.js"></script>
<script src="/static/events.js"></script>
<script src="/static/dom.js"></script>
<script src="/static/toast.js"></script>
<script src="/static/admin_library.js"></script>
Change to:
<script src="/static/main.js"></script>
5. templates/dashboard.templ
File: templates/dashboard.templ (310 lines)
Remove first script block (lines 15-19):
<script src="/static/api.js"></script>
<script src="/static/events.js"></script>
<script src="/static/dom.js"></script>
<script src="/static/toast.js"></script>
<script src="/static/storage.js"></script>
DELETE these lines entirely.
Replace dashboard.js script (line 80):
<script src="/static/dashboard.js"></script>
Change to:
<script src="/static/main.js"></script>
6. templates/header.templ
File: templates/header.templ (188 lines)
Replace script block (lines 184-187):
<script src="/static/storage.js"></script>
<script src="/static/theme.js"></script>
<script src="/static/header.js"></script>
<script src="/static/themeDropdown.js"></script>
Change to:
<script src="/static/main.js"></script>
Update theme dropdown (lines 48-93): This is a perfect Alpine use case - replace with Alpine component.
Current structure (simplified):
<button onclick="toggleThemeDropdown()">Theme</button>
<div id="theme-dropdown" class="hidden">
<button onclick="changeThemeTo('tokyo-night')">Tokyo Night</button>
<button onclick="changeThemeTo('dracula')">Dracula</button>
<!-- ... more themes ... -->
</div>
Change to Alpine:
<div x-data="themeDropdown">
<button @click="toggle()">Theme</button>
<div x-show="open"
@click.away="open = 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-56 rounded-lg shadow-lg z-50"
:class="open ? '' : 'hidden'">
<div class="py-1">
<button @click="changeTheme('tokyo-night')"
class="block w-full text-left px-3 py-2 rounded hover:opacity-80">
Tokyo Night
</button>
<button @click="changeTheme('dracula')"
class="block w-full text-left px-3 py-2 rounded hover:opacity-80">
Dracula
</button>
<!-- ... more themes ... -->
</div>
</div>
</div>
Update user menu (lines 57-60, 65):
<!-- Before -->
<button onclick="toggleUserMenu()">
<div id="user-menu" class="hidden">
<button onclick="logout()">Logout</button>
Change to:
<div x-data="{ userMenuOpen: false }">
<button @click="userMenuOpen = !userMenuOpen">
<div x-show="userMenuOpen"
@click.away="userMenuOpen = false"
x-transition>
<button @click="api.post('/api/auth/logout').then(() => window.location.href = '/login')">
Logout
</button>
</div>
</div>
7. templates/analytics.templ
File: templates/analytics.templ (100 lines)
Remove first script block (lines 10-14):
<script src="/static/api.js"></script>
<script src="/static/events.js"></script>
<script src="/static/dom.js"></script>
<script src="/static/toast.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
DELETE these lines entirely.
Note: Chart.js remains on CDN (intentionally not bundled - 3.4MB, only used on 1 page).
Replace analytics.js script (line 97):
<script src="/static/analytics.js"></script>
Change to:
<script src="/static/main.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
8. templates/bookshelf.templ
File: templates/bookshelf.templ (266 lines)
Remove first script block (lines 10-13):
<script src="/static/api.js"></script>
<script src="/static/events.js"></script>
<script src="/static/toast.js"></script>
<script src="/static/dom.js"></script>
DELETE these lines entirely.
Replace bookshelf.js script (line 63):
<script src="/static/bookshelf.js"></script>
Change to:
<script src="/static/main.js"></script>
9. templates/devices.templ
File: templates/devices.templ (870 lines)
Replace script block (lines 12-16):
<script src="/static/api.js"></script>
<script src="/static/events.js"></script>
<script src="/static/toast.js"></script>
<script src="/static/dom.js"></script>
<script src="/static/devices.js"></script>
Change to:
<script src="/static/main.js"></script>
10. templates/index.templ
File: templates/index.templ (165 lines)
Remove first script block (lines 10-12):
<script src="/static/api.js"></script>
<script src="/static/events.js"></script>
<script src="/static/toast.js"></script>
DELETE these lines entirely.
Replace index.js script (line 138):
<script src="/static/index.js"></script>
Change to:
<script src="/static/main.js"></script>
11. templates/login.templ
File: templates/login.templ (81 lines)
Replace script block (lines 10-12):
<script src="/static/api.js"></script>
<script src="/static/toast.js"></script>
<script src="/static/login.js"></script>
Change to:
<script src="/static/main.js"></script>
12. templates/profile.templ
File: templates/profile.templ (68 lines)
Replace script block (lines 9-11):
<script src="/static/api.js"></script>
<script src="/static/toast.js"></script>
<script src="/static/profile.js"></script>
Change to:
<script src="/static/main.js"></script>
13. templates/queue.templ
File: templates/queue.templ (169 lines)
Remove first script block (lines 12-15):
<script src="/static/api.js"></script>
<script src="/static/events.js"></script>
<script src="/static/toast.js"></script>
<script src="/static/dom.js"></script>
DELETE these lines entirely.
Replace queue.js script (line 166):
<script src="/static/queue.js"></script>
Change to:
<script src="/static/main.js"></script>
14. templates/conflicts.templ
File: templates/conflicts.templ (239 lines)
Remove first script block (lines 12-15):
<script src="/static/api.js"></script>
<script src="/static/events.js"></script>
<script src="/static/toast.js"></script>
<script src="/static/dom.js"></script>
DELETE these lines entirely.
Replace conflicts.js script (line 234):
<script src="/static/conflicts.js"></script>
Change to:
<script src="/static/main.js"></script>
15. templates/custom_section.templ
File: templates/custom_section.templ (168 lines)
Replace script block (lines 10-13):
<script src="/static/api.js"></script>
<script src="/static/events.js"></script>
<script src="/static/toast.js"></script>
<script src="/static/custom_section.js"></script>
Change to:
<script src="/static/main.js"></script>
16. templates/admin_users.templ
File: templates/admin_users.templ (145 lines)
Replace script block (lines 9-12):
<script src="/static/api.js"></script>
<script src="/static/events.js"></script>
<script src="/static/toast.js"></script>
<script src="/static/admin_users.js"></script>
Change to:
<script src="/static/main.js"></script>
17. templates/collection_rules.templ
File: templates/collection_rules.templ (420 lines)
Replace script block (lines 10-12):
<script src="/static/api.js"></script>
<script src="/static/events.js"></script>
<script src="/static/collection_rules.js"></script>
Change to:
<script src="/static/main.js"></script>
18. templates/progress.templ
File: templates/progress.templ (114 lines)
Replace script block (lines 12-14):
<script src="/static/api.js"></script>
<script src="/static/events.js"></script>
<script src="/static/progress.js"></script>
Change to:
<script src="/static/main.js"></script>
19. templates/register.templ
File: templates/register.templ (97 lines)
Replace script block (lines 10-12):
<script src="/static/api.js"></script>
<script src="/static/toast.js"></script>
<script src="/static/register.js"></script>
Change to:
<script src="/static/main.js"></script>
20. templates/settings.templ
File: templates/settings.templ (157 lines)
Replace script block (lines 9-11):
<script src="/static/api.js"></script>
<script src="/static/toast.js"></script>
<script src="/static/settings.js"></script>
Change to:
<script src="/static/main.js"></script>
21. templates/stats.templ
File: templates/stats.templ (162 lines)
Replace script block (lines 9-11):
<script src="/static/api.js"></script>
<script src="/static/events.js"></script>
<script src="/static/stats.js"></script>
Change to:
<script src="/static/main.js"></script>
22. templates/sync.templ
File: templates/sync.templ (184 lines)
Replace script block (lines 9-12):
<script src="/static/api.js"></script>
<script src="/static/events.js"></script>
<script src="/static/toast.js"></script>
<script src="/static/sync.js"></script>
Change to:
<script src="/static/main.js"></script>
23. templates/testing_templ.templ
File: templates/testing_templ.templ (115 lines)
Replace script block (lines 9-11):
<script src="/static/api.js"></script>
<script src="/static/events.js"></script>
<script src="/static/testing_templ.js"></script>
Change to:
<script src="/static/main.js"></script>
24. themes/obsidian.templ
File: themes/obsidian.templ (91 lines)
Replace script block (lines 9-11):
<script src="/static/api.js"></script>
<script src="/static/toast.js"></script>
<script src="/static/obsidian.js"></script>
Change to:
<script src="/static/main.js"></script>
25. themes/whatsapp.templ
File: themes/whatsapp.templ (86 lines)
Replace script block (lines 9-11):
<script src="/static/api.js"></script>
<script src="/static/toast.js"></script>
<script src="/static/whatsapp.js"></script>
Change to:
<script src="/static/main.js"></script>
26. themes/midnight.templ
File: themes/midnight.templ (91 lines)
Replace script block (lines 9-11):
<script src="/static/toast.js"></script>
<script src="/static/api.js"></script>
<script src="/static/midnight.js"></script>
Change to:
<script src="/static/main.js"></script>
27. themes/sunset.templ (same pattern as other themes)
File: themes/sunset.templ (86 lines)
Replace script block (lines 9-11):
<script src="/static/toast.js"></script>
<script src="/static/api.js"></script>
<script src="/static/sunset.js"></script>
Change to:
<script src="/static/main.js"></script>
Remaining Templates (7-26)
For templates 7-26, follow this pattern:
- Remove all individual script tags (usually 2-5 scripts)
- Replace with single script:
<script src="/static/main.js"></script> - Replace
onclickwith@click - Add
x-datafor stateful components (modals, dropdowns)
Quick find-replace patterns:
# In each template file:
onclick="funcName()" → @click="module.funcName()"
onclick="func('param')" → @click="module.func('param')"
class="hidden" → x-show="!isOpen" (with x-data parent)
Common stateful patterns:
Modal pattern:
<!-- Before -->
<button onclick="showModal()">Open</button>
<div id="modal" class="hidden">
<button onclick="hideModal()">Close</button>
</div>
<!-- After -->
<div x-data="{ modalOpen: false }">
<button @click="modalOpen = true">Open</button>
<div x-show="modalOpen" x-transition>
<button @click="modalOpen = false">Close</button>
</div>
</div>
Confirm delete pattern:
<!-- Before -->
<button onclick="confirmDelete()">Delete</button>
<!-- After -->
<button @click="if(confirm('Are you sure?')) module.deleteItem()">Delete</button>
Form submission (keep HTMX for server calls):
<!-- Keep HTMX for forms -->
<form hx-post="/api/save" hx-target="#result">
<!-- Alpine for client-side validation -->
<input x-model="formData.name" @input="validate = true">
<span x-show="validate && !formData.name" class="error">Name required</span>
</form>
File: themes/sunset.templ (86 lines)
Replace script block (lines 9-11):
<script src="/static/toast.js"></script>
<script src="/static/api.js"></script>
<script src="/static/sunset.js"></script>
Change to:
<script src="/static/main.js"></script>
Phase 4: Build and Verify
Step 1: Create Alpine Registration File
File: web/src/alpine.ts (already exists, needs update)
Current file (missing window.Alpine):
import Alpine from "alpinejs";
// Initialize Alpine
Alpine.start();
// Re-export Alpine for other modules to use
export { Alpine };
Change to (add window.Alpine + type declaration):
import Alpine from "alpinejs";
// Extend Window interface to include Alpine
declare global {
interface Window {
Alpine: typeof Alpine;
}
}
// Initialize Alpine
window.Alpine = Alpine;
Alpine.start();
// Re-export Alpine for other modules to use
export { Alpine };
Why type declaration: Fixes TypeScript error "Property 'Alpine' does not exist on type 'Window'"
Why window.Alpine: Required for Alpine DevTools browser extension to work.
Step 2: Update main.ts
File: web/src/main.ts
Add at end (after all other imports):
import './alpine';
Step 3: Build the Bundle
npm run build:ts
Expected output: Creates web/static/main.js (~350-450KB unminified, ~120-150KB minified with Alpine bundled) + main.js.map sourcemap
Why larger: Alpine.js adds ~15KB gzipped to bundle (worth it for cleaner code).
Step 4: Regenerate Templates
templ generate
Expected output: Regenerates all .templ files with updated Alpine directives
Step 5: Verify Alpine is Loaded
head -n 50 web/static/main.js | grep -i alpine
Check for: Alpine initialization code present in bundle.
Step 6: Test in Browser
-
Start dev server:
go run . -
Open browser DevTools (F12) → Console
-
Verify Alpine loaded:
typeof window.Alpine !== 'undefined' // should be true -
Test key pages:
- Dashboard (tests theme dropdown, wood paneling)
- Docs page (tests lunr search, sidebar toggle)
- Collections (tests modal, multiple onclick handlers)
- Header (tests theme dropdown, user menu)
- Devices (tests multiple modals)
-
Check Alpine DevTools (optional):
- Install Alpine DevTools browser extension
- Inspect
x-datacomponents in DevTools panel - Verify reactive state changes
Step 7: Verify Functionality
In browser console, test Alpine globals:
// Should all be accessible via Alpine
Alpine.stores?.toast // Toast store
Alpine.global('showToast') // Global function
Test interactions:
- Theme dropdown opens/closes smoothly
- Modals open with transitions
- Toast notifications appear
- Forms still submit via HTMX
- Search works on docs page
- All 151 onclick handlers work with
@click
Step 8: Check Bundle Size
ls -lh web/static/main.js
Expected: ~120-150KB (minified with Alpine included)
Breakdown:
- App code: ~100KB
- Alpine.js: ~15KB gzipped
- Lunr: ~10KB gzipped
- Highlight.js: ~5KB gzipped
- Total: ~130KB / ~40KB gzipped
Phase 5: Migrate TypeScript Files to Alpine (Step-by-Step)
Priority 1: Core Utilities (Do These First)
1. web/src/toast.ts
Current state: Already has Alpine import at line 1, but not using it yet
Add import (already at line 1):
import { Alpine } from "./alpine";
Replace export (lines 229-236):
// Before
(window as any).showToast = {
error: (message: string, duration?: number) =>
showToast(message, "error", duration),
success: (message: string, duration?: number) =>
showToast(message, "success", duration),
info: (message: string, duration?: number) =>
showToast(message, "info", duration),
};
Change to:
// After - Register toast API with Alpine
Alpine.global('showToast', {
error: (message: string, duration?: number) =>
showToast(message, "error", duration),
success: (message: string, duration?: number) =>
showToast(message, "success", duration),
info: (message: string, duration?: number) =>
showToast(message, "info", duration),
});
Template usage:
<!-- Before -->
<button onclick="showToast.success('Saved!')">Save</button>
<!-- After -->
<button @click="showToast.success('Saved!')">Save</button>
2. web/src/api.ts
Add import (at top):
import { Alpine } from './alpine';
Replace export (lines 90-99):
// Before
(window as any).api = {
get: apiGet,
post: apiPost,
put: apiPut,
delete: apiDelete,
patch: apiPatch,
handleResponse,
handleVoidResponse,
handleError,
};
Change to:
// After - Register API with Alpine
Alpine.global('api', {
get: apiGet,
post: apiPost,
put: apiPut,
delete: apiDelete,
patch: apiPatch,
handleResponse,
handleVoidResponse,
handleError,
});
Also update line 85-87 (toast error calls):
// Before
if ((window as any).showToast?.error) {
(window as any).showToast.error(message);
}
// After (will work once toast.ts is migrated)
if (window.Alpine?.stores?.toast?.error) {
window.Alpine.stores.toast.error(message);
}
// Or keep using window.showToast during migration
Template usage:
<!-- Before -->
<button onclick="api.post('/api/save', data)">Save</button>
<!-- After -->
<button @click="api.post('/api/save', data)">Save</button>
3. web/src/storage.ts
Add import (at top):
import { Alpine } from './alpine';
Replace export (lines 53-67):
// Before
(window as any).storage = {
getToken,
setToken,
removeToken,
getRefreshToken,
setRefreshToken,
removeRefreshToken,
getTheme,
setTheme,
getSelectedLibrary,
setSelectedLibrary,
getSelectedBook,
setSelectedBook,
clearAll,
};
Change to:
// After - Register storage helpers with Alpine
Alpine.global('storage', {
getToken,
setToken,
removeToken,
getRefreshToken,
setRefreshToken,
removeRefreshToken,
getTheme,
setTheme,
getSelectedLibrary,
setSelectedLibrary,
getSelectedBook,
setSelectedBook,
clearAll,
});
Template usage:
<!-- Before -->
<button onclick="const token = storage.getToken()">Get Token</button>
<!-- After -->
<button @click="const token = storage.getToken()">Get Token</button>
Priority 2: Stateful Components
4. web/src/header.ts (Theme Dropdown)
Add import (at top):
import { Alpine } from './alpine';
import { changeThemeTo, loadTheme, applyTheme } from './theme';
Add Alpine component (at end of file):
Alpine.data('themeDropdown', () => ({
open: false,
toggle() {
this.open = !this.open;
},
changeTheme(theme: string) {
changeThemeTo(theme);
this.open = false;
},
init() {
const savedTheme = loadTheme();
if (savedTheme) applyTheme(savedTheme);
}
}));
Template usage:
<div x-data="themeDropdown">
<button @click="toggle()">Theme</button>
<div x-show="open" @click.away="open = false" x-transition>
<button @click="changeTheme('tokyo-night')">Tokyo Night</button>
</div>
</div>
5. web/src/woodPaneling.ts
Add Alpine component (at end of file):
import { Alpine } from './alpine';
Alpine.data('woodPaneling', () => ({
current: localStorage.getItem('woodPaneling') || 'none',
change(style: string) {
this.current = style;
localStorage.setItem('woodPaneling', style);
// Apply logic here...
}
}));
Template usage:
<div x-data="woodPaneling">
<button @click="change('none')" :class="current === 'none' ? 'active' : ''">None</button>
<button @click="change('wood-light')" :class="current === 'wood-light' ? 'active' : ''">Wood Light</button>
</div>
Priority 3: Page-Specific Functions
6. web/src/collections.ts
Add import:
import { Alpine } from './alpine';
Register as namespace (at end):
Alpine.global('collections', {
back: backToCollections,
showAddModal: showAddBooksModal,
hideAddModal: hideAddBooksModal,
addSelected: addSelectedBooks,
removeBook: removeBook,
navigate: navigateToCollection
});
7. web/src/devices.ts, queue.ts, conflicts.ts
Follow same pattern as collections.ts - register as namespace with Alpine.global()
Phase 6: Clean Up (Optional)
Remove Individual JS Files
After verifying everything works, remove old individual .js files:
rm web/static/api.js
rm web/static/events.js
rm web/static/dom.js
rm web/static/toast.js
rm web/static/storage.js
rm web/static/theme.js
rm web/static/header.js
rm web/static/themeDropdown.js
rm web/static/woodPaneling.js
rm web/static/search.js
rm web/static/docs.js
rm web/static/collections.js
rm web/static/conflicts.js
rm web/static/dashboard.js
rm web/static/admin*.js
rm web/static/analytics.js
rm web/static/bookshelf.js
rm web/static/devices.js
rm web/static/index.js
rm web/static/login.js
rm web/static/profile.js
rm web/static/queue.js
rm web/static/custom_section.js
rm web/static/progress.js
rm web/static/register.js
rm web/static/settings.js
rm web/static/stats.js
rm web/static/sync.js
rm web/static/testing_templ.js
rm web/static/obsidian.js
rm web/static/whatsapp.js
rm web/static/midnight.js
rm web/static/sunset.js
Note: Keep main.js and main.js.map
Troubleshooting
Issue: "Alpine is not defined"
Cause: Alpine not initialized or alpine.ts not imported in main.ts
Fix: Ensure import './alpine'; is at end of main.ts and Alpine.start() is called
Issue: "Alpine.global is not a function"
Cause: Using old Alpine syntax
Fix: Use Alpine.data() for components or register globals before Alpine.start()
Issue: "lunr is not defined"
Cause: Missing import in docs.ts
Fix: Ensure import * as lunr from 'lunr'; is at line 4
Issue: "hljs is not defined"
Cause: Missing import in docs.ts
Fix: Ensure import hljs from 'highlight.js'; is at line 5
Issue: @click handlers not working
Cause: Alpine not loaded or syntax error in directive
Fix: Check browser console, verify Alpine initialized, check directive syntax
Issue: x-show elements always visible
Cause: Missing x-cloak CSS or Alpine not loaded before DOM ready
Fix: Add [x-cloak] { display: none !important; } to CSS, ensure Alpine loads early
Issue: Bundle too large (>200KB minified)
Cause: Check if large dependencies accidentally bundled
Fix: Verify Chart.js is NOT bundled (should remain CDN link in analytics.templ)
Issue: 404 errors for .js files
Cause: Script tags not updated in templates
Fix: Check that all template script tags point to /static/main.js
Issue: Theme dropdown doesn't close
Cause: Missing @click.away directive
Fix: Add @click.away="open = false" to dropdown element
Issue: Search not working on docs page
Cause: lunr not properly imported or bundled
Fix: Check docs.ts imports and rebuild with npm run build:ts
Issue: Modals don't open
Cause: x-show variable not reactive or parent x-data missing
Fix: Ensure modal is wrapped in x-data="{ modalOpen: false }" and button uses @click="modalOpen = true"
Issue: HTMX forms stopped working after Alpine migration
Cause: Alpine event handlers conflicting with HTMX
Fix: HTMX and Alpine work together - ensure hx-post is on form, @click is on buttons (not form)
External Dependencies (Not Bundled)
Chart.js
File: templates/analytics.templ (line 14)
Reason: 3.4MB minified, only used on 1 page (analytics)
Approach: Keep as CDN link: <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
htmx.org
File: Loaded via separate script tag in base templates
Reason: Core framework, needs to load before main.js
Approach: Keep as separate script tag (not bundled), works great with Alpine
Architecture Notes
Why Alpine.js Over Window Exports?
Alpine is the modern approach for this codebase:
- Reactive state management:
x-data,x-show,x-modelinstead of manual DOM manipulation - Clean templates:
@clickinstead ofonclick="window.func()" - Component-based:
Alpine.data()for reusable components - SSR-friendly: Works with progressive enhancement
- No global pollution: Functions registered in Alpine scope, not window
- Better DX: Alpine DevTools for debugging reactive state
Hybrid Approach: Alpine + HTMX
- Alpine: Client-side state (modals, dropdowns, theme, form validation)
- HTMX: Server calls (form submissions, API calls, data fetching)
- Why both: HTMX excels at server communication, Alpine excels at client-side state
ESBuild + Alpine Benefits
- Single bundle: Alpine bundled with app code (~15KB gzipped)
- No CDN dependency: Faster load, no network request for Alpine
- Tree-shaking: Unused Alpine features not included
- ES2020 target: Works on Chrome 80+, Firefox 72+, Safari 13.1+
Migration Path: Window → Alpine
Old approach:
(window as any).showToast = showToast;
<button onclick="showToast('Saved!')">Save</button>
New approach:
Alpine.global('showToast', showToast);
<button @click="showToast('Saved!')">Save</button>
Why Not Put Everything in main.ts?
Main.ts stays simple (24 lines) because:
- Imports where used: docs.ts imports lunr, not main.ts
- Tree-shaking: Only what's used gets bundled
- Maintainability: Each file has its own dependencies
- Testing: Can test individual modules independently
- Alpine registration: Each file registers its own Alpine globals/components
Success Criteria
✅ Single main.js bundle (~130KB minified with Alpine)
✅ All 27 templates use single script tag
✅ No 404 errors for missing .js files
✅ Alpine.js loaded and functional
✅ No (window as any) usage in TypeScript
✅ Lunr search works on docs page
✅ Syntax highlighting works on docs page
✅ Theme dropdown opens/closes with Alpine
✅ Modals use x-show/x-transition
✅ All @click handlers work (151 migrated)
✅ Toast notifications work via Alpine
✅ HTMX forms still submit correctly
✅ Chart.js loads on analytics page (CDN)
✅ Browser console shows no errors
✅ Alpine DevTools shows reactive components
✅ Bundle targets ES2020 for broad compatibility
✅ Sourcemap generated for debugging
Post-Migration: Development Workflow
Watch Mode (Development)
npm run dev
Watches both TypeScript and templates, rebuilds on changes.
Production Build
npm run build:ts
templ generate
go build
Creates minified bundle, regenerates templates, builds Go binary.
Debugging
Use main.js.map sourcemap in browser DevTools to debug original TypeScript sources.
Files Modified Summary
New Files Created
web/src/alpine.ts(new) - Alpine initialization and exports
Configuration Files
- ✅ package.json (already correct - no changes needed)
- ✅ tsconfig.json (already ES2020 - no changes needed)
Source Files Modified
web/src/main.ts(addimport './alpine'at end)web/src/docs.ts(remove 4 window globals, add Alpine registration)web/src/toast.ts(replace window export with Alpine.global)web/src/api.ts(replace window export with Alpine.global)web/src/storage.ts(replace window export with Alpine.global)web/src/events.ts(replace window export with Alpine.global)web/src/header.ts(add Alpine.data themeDropdown component)web/src/woodPaneling.ts(add Alpine.data component)web/src/collections.ts(register namespace with Alpine.global)web/src/conflicts.ts(register namespace with Alpine.global)web/src/devices.ts(register namespace with Alpine.global)web/src/queue.ts(register namespace with Alpine.global)web/src/search.ts(register namespace with Alpine.global)web/src/dom.ts(register namespace with Alpine.global)- Plus 10+ more TypeScript files (register functions with Alpine)
Template Files Modified (27 files)
All templates updated to:
- Remove individual script tags
- Use single
<script src="/static/main.js"></script> - Replace
onclickwith@click - Add
x-datafor stateful components (modals, dropdowns)
Templates:
- templates/collections.templ (migrate 7 onclick handlers)
- templates/docs.templ (migrate sidebar toggle to Alpine)
- templates/admin.templ
- templates/admin_library.templ
- templates/dashboard.templ (theme dropdown, wood paneling)
- templates/header.templ (theme dropdown, user menu - KEY FILE)
- templates/analytics.templ (keep Chart.js CDN)
- templates/bookshelf.templ
- templates/devices.templ (multiple modals)
- templates/index.templ
- templates/login.templ
- templates/profile.templ
- templates/queue.templ
- templates/conflicts.templ
- templates/custom_section.templ
- templates/admin_users.templ
- templates/collection_rules.templ
- templates/progress.templ
- templates/register.templ
- templates/settings.templ
- templates/stats.templ
- templates/sync.templ
- templates/testing_templ.templ
- themes/obsidian.templ
- themes/whatsapp.templ
- themes/midnight.templ
- themes/sunset.templ
Generated Files
web/static/main.js(new bundled output with Alpine)web/static/main.js.map(new sourcemap)
Files to Delete (After Verification)
All individual .js files in web/static/ (except main.js and main.js.map):
- api.js, events.js, dom.js, toast.js, storage.js
- theme.js, header.js, themeDropdown.js, woodPaneling.js
- docs.js, search.js, collections.js, conflicts.js
- dashboard.js, admin*.js, analytics.js, bookshelf.js
- devices.js, index.js, login.js, profile.js, queue.js
- custom_section.js, progress.js, register.js, settings.js
- stats.js, sync.js, testing_templ.js
- obsidian.js, whatsapp.js, midnight.js, sunset.js
Migration Checklist
Use this checklist to track progress:
Phase 1: Clean Up docs.ts
- Add imports for lunr and hljs (lines 3-4)
- Remove
(window as any).lunrcheck (line 50) - Replace
(window as any).lunrIndexwithlunr.Builder.loadJs(line 56) - Replace
(window as any).docsDatawithdocsimport (line 73) - Replace window export with Alpine registration (line 99)
Phase 2: Create Alpine Infrastructure
- Create
web/src/alpine.tswith Alpine initialization - Add
import './alpine'toweb/src/main.ts - Build and verify Alpine is loaded (
npm run build:ts)
Phase 3: Migrate Core TypeScript Files (Priority 1)
web/src/toast.ts- Register with Alpine.globalweb/src/api.ts- Register with Alpine.globalweb/src/storage.ts- Register with Alpine.globalweb/src/events.ts- Register with Alpine.globalweb/src/dom.ts- Register with Alpine.global
Phase 4: Migrate Stateful Components (Priority 2)
web/src/header.ts- Create themeDropdown Alpine.data componentweb/src/woodPaneling.ts- Create woodPaneling Alpine.data component- Test theme dropdown in browser
- Test wood paneling in browser
Phase 5: Migrate Page-Specific Files (Priority 3)
web/src/collections.ts- Register namespaceweb/src/devices.ts- Register namespaceweb/src/queue.ts- Register namespaceweb/src/conflicts.ts- Register namespaceweb/src/search.ts- Register namespace
Phase 6: Update Templates (27 files)
- templates/header.templ (CRITICAL - theme dropdown)
- templates/docs.templ (sidebar toggle)
- templates/dashboard.templ (wood paneling)
- templates/collections.templ (7 onclick handlers)
- templates/admin.templ through themes/sunset.templ (23 remaining)
- Run
templ generateafter template changes
Phase 7: Test and Verify
- Build:
npm run build:ts - Check bundle size: ~130KB minified
- Start dev server:
go run . - Test all 151 onclick handlers
- Verify Alpine DevTools shows components
- Test HTMX forms still work
- Check browser console for errors
- Test on Chrome, Firefox, Safari (ES2020 compatibility)
Phase 8: Clean Up
- Delete all individual .js files from web/static/
- Commit changes
- Deploy and test in production
Additional Resources
Alpine.js Documentation
- Official docs: https://alpinejs.dev/
- Essentials guide: https://alpinejs.dev/essentials/start
x-datadocs: https://alpinejs.edu/directives/data@clickdocs: https://alpinejs.edu/directives/onx-showdocs: https://alpinejs.edu/directives/showx-transitiondocs: https://alpinejs.edu/directives/transition
Alpine + HTMX Integration
- Blog post: https://htmx.org/examples/blog/
- Both work together seamlessly - Alpine for client state, HTMX for server calls
ESBuild Documentation
- Official docs: https://esbuild.github.io/
- API: https://esbuild.github.io/api/
- Bundling: https://esbuild.github.io/api/#bundle
Migration Tips
- Start small: Migrate 1-2 files at a time, test frequently
- Use Alpine DevTools: Install browser extension for debugging
- Keep HTMX for forms: Don't replace
hx-postwith Alpine fetch - Test in browser: Console errors will show missing Alpine globals
- Progressive migration: Can use window exports AND Alpine during transition
Configuration
- ✅ package.json (already correct)
- ✅ tsconfig.json (already ES2020)
Source Files
web/src/docs.ts(add imports, remove 4 window globals)
Template Files (27 files)
All templates updated to use single <script src="/static/main.js"></script> tag:
- templates/collections.templ
- templates/docs.templ
- templates/admin.templ
- templates/admin_library.templ
- templates/dashboard.templ
- templates/header.templ
- templates/analytics.templ
- templates/bookshelf.templ
- templates/devices.templ
- templates/index.templ
- templates/login.templ
- templates/profile.templ
- templates/queue.templ
- templates/conflicts.templ
- templates/custom_section.templ
- templates/admin_users.templ
- templates/collection_rules.templ
- templates/progress.templ
- templates/register.templ
- templates/settings.templ
- templates/stats.templ
- templates/sync.templ
- templates/testing_templ.templ
- themes/obsidian.templ
- themes/whatsapp.templ
- themes/midnight.templ
- themes/sunset.templ
Generated Files
web/static/main.js(new bundled output)web/static/main.js.map(new sourcemap)
Files to Delete (After Verification)
All individual .js files in web/static/ (except main.js and main.js.map)
Summary: What We're Achieving
The Problem
- 151 inline
onclickhandlers usingwindow.funcName() - 20+ TypeScript files exporting to
(window as any) - 27 template files with 3-5 script tags each
- Manual DOM manipulation for state (modals, dropdowns)
- No reactive state management
- Global namespace pollution
The Solution
- Bundle with ESBuild: Single ~130KB minified bundle (ES2020 target)
- Alpine.js for state: Reactive components with
x-data,@click,x-show - Keep HTMX for forms: Server-side communication remains unchanged
- Modern patterns: No more
(window as any), clean templates
Benefits
- ✅ Performance: Single HTTP request for all JS, better caching
- ✅ Maintainability: Alpine components vs imperative DOM manipulation
- ✅ Type safety: No more
window as anyhacks - ✅ Developer experience: Clean templates, Alpine DevTools, sourcemaps
- ✅ User experience: Smooth transitions, reactive UI, faster loads
- ✅ Bundle size: Only ~40KB gzipped (was 30+ HTTP requests before)
Key Architecture Decisions
- Alpine over vanilla event listeners: Reactive state is cleaner
- Alpine over React/Vue: Lightweight, SSR-friendly, works with HTMX
- Bundle Alpine: No CDN dependency, faster load, tree-shaking
- Keep HTMX: Don't fix what works - HTMX excels at server communication
- Progressive migration: Migrate incrementally, can mix old/new during transition
Migration Effort
- TypeScript files: 20+ files (5-10 min each) = ~3 hours
- Templates: 27 files (10-15 min each) = ~5 hours
- Testing: 2-3 hours
- Total: ~10-12 hours for complete migration
Maintenance Going Forward
- Adding new feature? Create Alpine component or register function
- New page? Single
<script src="/static/main.js">tag - Need state?
x-data="{ open: false }" - Server call?
hx-post="/api/endpoint"(HTMX)
Quick Reference: Alpine Syntax
| Old (Window) | New (Alpine) |
|---|---|
onclick="func()" |
@click="func()" |
onclick="obj.method()" |
@click="obj.method()" |
class="hidden" |
x-show="!open" |
| Manual DOM toggle | x-data="{ open: false }" + @click="open = !open" |
onclick="window.api.get()" |
@click="api.get()" |
| Form validation | x-model="formData.name" + <span x-show="!name">Error</span> |
Last Updated: March 2026
ESBuild Version: Latest (via npm)
Alpine.js Version: Latest (via npm)
Target: ES2020 (Chrome 80+, Firefox 72+, Safari 13.1+)