diff --git a/esbuild-setup.md b/esbuild-setup.md index 2f5772a..3f6ad61 100644 --- a/esbuild-setup.md +++ b/esbuild-setup.md @@ -1,719 +1,1953 @@ -# ESBuild Setup and Migration Guide +# 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+). -This guide walks through setting up ESBuild properly for Bookhoard, eliminating `(window as any)` globals, and following project guidelines (procedural TypeScript, SSR-first, progressive enhancement). - -**Target**: Single `main.js` bundle (~100KB minified) with ES2020 support for broad browser compatibility. +**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) --- -## Table of Contents +## Status: What's Already Done ✅ -1. [Current State Assessment](#current-state-assessment) -2. [ESBuild Configuration](#esbuild-configuration) -3. [Package.json Setup](#packagejson-setup) -4. [Removing Globals](#removing-globals) -5. [Import Strategy](#import-strategy) -6. [Template Updates](#template-updates) -7. [Build and Test](#build-and-test) -8. [Verification Checklist](#verification-checklist) +### 1. package.json Dependencies (Already Correct) +**File**: `package.json` +**Status**: ✅ Already configured - NO CHANGES NEEDED ---- +Dependencies (lines 14-26): +- `htmx.org`, `alpinejs`, `lunr`, `highlight.js` already in dependencies +- `esbuild`, `typescript`, TailwindCSS tooling already in devDependencies -## Current State Assessment - -### What We Have - -- **29 TypeScript files** in `web/src/` (no npm imports currently) -- **Existing `main.ts`** that imports all modules (good foundation!) -- **Separate unbundled .js files** in `web/static/` (causes issues) -- **404 errors** for htmx.min.js, lunr.min.js, highlight.min.js (not copied from node_modules) -- **`(window as any)` globals** in docs.ts (antipattern with bundler) - -### What We Need - -- ✅ Single `main.js` bundle from `web/src/main.ts` -- ✅ ESBuild minification with `--target=es2020` -- ✅ Direct imports of npm packages (no globals) -- ✅ Keep imports where used (don't overwhelm main.ts) -- ✅ Progressive enhancement (pages work without JS) - ---- - -## ESBuild Configuration - -### Why ESBuild? - -- **Go-based** (matches templ/sqlc philosophy) -- **10-100x faster** than Webpack -- **Small node_modules** (~6MB vs 44MB with Vite) -- **Automatic tree-shaking and minification** -- **Single-pass compilation** (no separate TypeScript step needed) - -### ES2020 Target +### 2. Build Scripts (Already Correct) +**File**: `package.json` +**Status**: ✅ Already configured - NO CHANGES NEEDED +Lines 8-10: ```json -"--target=es2020" +"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\"", ``` -**Why not esnext?** -- ES2020 supports browsers from 2020+ (Chrome 80+, Firefox 72+, Safari 13.1+) -- Covers Ubuntu 22.04 LTS users (supported until 2027) -- Covers lightweight browsers (Ephiphany, Falkon with recent Qt/WebKit) -- Avoids bug reports from users with slightly older browsers +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. --- -## package.json Setup +## Phase 1: Clean Up docs.ts -### Current State (Already Correct!) +**File**: `web/src/docs.ts` (99 lines) +**Status**: Imports already added ✅ +**Lines to modify**: 50, 56, 73 (remove window globals), 99 (replace with Alpine) -Your `package.json` already has perfect dependency classification: +**Note**: Lines 3-4 already have the correct imports: +```typescript +import * as lunr from "lunr"; +import hljs from "highlight.js"; +``` -```json -{ - "dependencies": { - "htmx.org": "^2.0.8", - "alpinejs": "^3.15.8", - "lunr": "^2.3.9", - "highlight.js": "^11.11.1" - }, - "devDependencies": { - "esbuild": "^0.27.3", - "typescript": "^5.9.3", - "@tailwindcss/forms": "^0.5.11", - "@tailwindcss/typography": "^0.5.19", - "autoprefixer": "^10.4.27", - "tailwindcss": "^3.4.19" +### Step 1: Remove (window as any).lunr check +**Location**: Line 50 + +**Current** (lines 49-53): +```typescript + if (!(window as any).lunr) { + console.warn("Lunr.js not loaded"); + return; } -} ``` -**Runtime dependencies** (bundled into main.js): -- ✅ htmx.org - Declarative AJAX framework -- ✅ alpinejs - Reactive UI components -- ✅ lunr - Full-text search -- ✅ highlight.js - Syntax highlighting - -**Build-time dependencies**: -- ✅ esbuild - Bundler -- ✅ typescript - Type checker -- ✅ tailwindcss/* - CSS tooling - -### Update Build Scripts - -Replace the current build scripts in `package.json`: - -```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 && npm run build:css" - } -} -``` - -**Key changes**: -- ✅ Bundle from `web/src/main.ts` (single entry point) -- ✅ Output to `web/static/main.js` (single file) -- ✅ Keep `--target=es2020` (browser compatibility) -- ✅ `--minify` for production builds -- ✅ Added convenience scripts (`build`, `dev`) - ---- - -## Removing Globals - -### Current Global Usage - -**File**: `web/src/docs.ts` - +**Change to**: ```typescript -// Line 45: Check if lunr loaded -if (!(window as any).lunr) { - console.warn("Lunr.js not loaded"); - return; -} - -// Line 51: Access search index -const idx = (window as any).lunrIndex; - -// Line 68: Access docs data -const doc = (window as any).docsData?.[result.ref]; - -// Line 94: Export function globally -(window as any).toggleSidebar = toggleSidebar; + // Lunr now bundled via ESBuild ``` -### Why This Exists - -Your current setup loads libraries via separate ` - -``` - -This attaches libraries to `window`, requiring type casts to access. - -### Three Types of Globals - -#### Type 1: Library Imports (Should be direct imports) - -**Example**: `(window as any).lunr` - -**Fix**: Import directly in files that use it +### Step 2: Replace (window as any).lunrIndex with direct lunr usage +**Location**: Line 56 +**Current** (lines 55-62): ```typescript -// web/src/docs.ts -import lunr from 'lunr'; - -// Use directly, no window needed -function buildSearchIndex(data: any[]) { - const idx = lunr(function() { - this.use(lunr.flex); - this.ref('id'); - this.field('title', {boost: 10}); - this.field('content', {boost: 1}); - data.forEach(doc => this.add(doc)); - }); - return idx; -} + const idx = (window as any).lunrIndex; + if (!idx) { + searchResults.innerHTML = + '

Search index not loaded

'; + searchResults.classList.remove("hidden"); + return; + } ``` -#### Type 2: SSR Data (Server-side rendered data) - -**Example**: `(window as any).lunrIndex`, `(window as any).docsData` - -These are **NOT library globals** - they're data fetched from the server at runtime. - -**Current approach**: Template fetches `/docs/search-index.json` and builds lunr index - -**Three options**: - -**Option A: Keep as globals with proper types** (simplest) - +**Change to**: ```typescript -// web/src/docs.ts -// Add global declaration -declare global { - interface Window { - lunrIndex: any; - docsData: Record; - } -} - -// Use without type casts -const idx = window.lunrIndex; -const doc = window.docsData?.[result.ref]; + const idx = lunr.Builder.loadJs(searchIndex); + if (!idx) { + searchResults.innerHTML = + '

Search index not loaded

'; + searchResults.classList.remove("hidden"); + return; + } ``` -**Option B: Data attributes** (cleaner, requires template changes) - -```go -// In Go template -
-``` +### Step 3: Replace (window as any).docsData with direct import +**Location**: Line 73 +**Current** (lines 70-76): ```typescript -// In TypeScript -const container = document.getElementById('search-container')!; -const searchIndex = JSON.parse(container.dataset.searchIndex!); -const docsData = JSON.parse(container.dataset.docsData!); + .map((result: { ref: string }) => { + const doc = (window as any).docsData?.[result.ref]; + if (!doc) return ""; ``` -**Option C: Loader module** (best, most maintainable) - +**Change to**: ```typescript -// web/src/search-data.ts (NEW FILE) -let lunrIndex: any = null; -let docsData: Record = {}; - -export async function loadSearchData() { - const response = await fetch('/docs/search-index.json'); - const data = await response.json(); - - // Build lunr index - lunrIndex = lunr(function() { - this.use(lunr.flex); - this.ref('id'); - this.field('title', {boost: 10}); - this.field('content', {boost: 1}); - data.forEach(doc => this.add(doc)); - }); - - docsData = data; -} - -export { lunrIndex, docsData }; + .map((result: { ref: string }) => { + const doc = docs[result.ref]; + if (!doc) return ""; ``` -```typescript -// web/src/docs.ts -import { lunrIndex, docsData, loadSearchData } from './search-data'; +### Step 4: Replace window export with Alpine global +**Location**: Line 99 -document.addEventListener('DOMContentLoaded', async () => { - await loadSearchData(); +**Current** (lines 98-99): +```typescript +document.addEventListener("DOMContentLoaded", () => { initializeDocsSearch(); }); -function performDocsSearch(query: string) { - // Use imported data, no globals - const idx = lunrIndex; - const doc = docsData[result.ref]; -} +(window as any).toggleSidebar = toggleSidebar; ``` -#### Type 3: Function Exports for HTML (Should use event listeners) - -**Example**: `(window as any).toggleSidebar = toggleSidebar;` - -**Why**: Template uses `onclick="toggleSidebar()"` - -**Fix**: Replace `onclick` attributes with `addEventListener` - +**Change to**: ```typescript -// Remove: (window as any).toggleSidebar = toggleSidebar; - -// Add event listener -document.addEventListener('click', (e) => { - const button = e.target.closest('[data-action="toggle-sidebar"]'); - if (button) toggleSidebar(); +document.addEventListener("DOMContentLoaded", () => { + initializeDocsSearch(); + + // Register with Alpine globally + if (typeof window.Alpine !== 'undefined') { + window.Alpine.effect(() => { + window.Alpine.global('docs', { + toggleSidebar + }); + }); + } }); ``` -```html - - -``` - -### Recommended Approach - -**Quick fix** (Types 1 + 2A): -1. Import libraries directly (`import lunr from 'lunr'`) -2. Add proper TypeScript declarations for SSR globals -3. Keep function exports for now (can fix later) - -**Best practice** (All types): -1. Import libraries directly -2. Use Option C (loader module) for SSR data -3. Replace all `onclick` with `addEventListener` +**Why Alpine.global()**: Makes `toggleSidebar()` available to Alpine templates via `@click="docs.toggleSidebar()"` --- -## Import Strategy +## Phase 2: Migrate TypeScript Files to Alpine Registration -### Principle: Import Where Used +**Approach**: Replace `(window as any)` exports with Alpine.js global registration -**❌ DON'T**: Import everything in main.ts +### Architecture Note: Alpine.js for Client-Side State +**Why Alpine over window exports**: +- Modern, reactive framework (already in package.json) +- Clean template syntax: `@click` instead of `onclick="window.func()"` +- Built-in state management: `x-data`, `x-show`, `x-model` +- Works with SSR (progressive enhancement) +- No global namespace pollution + +**Hybrid approach**: +- **Alpine**: Client-side state (modals, dropdowns, theme, forms) +- **HTMX**: Server calls (already using for form submissions) + +### Step 1: Create Alpine Registration Helper + +**New file**: `web/src/alpine.ts` ```typescript -// main.ts - DON'T DO THIS -import './collections'; -import './docs'; -import 'htmx.org'; -import 'alpinejs'; -import 'lunr'; -import 'highlight.js/lib/common'; -// ... 29 more imports -``` - -This overwhelms main.ts and makes it hard to see what each file needs. - -**✅ DO**: Import in files that use it - -```typescript -// main.ts - Keep simple! -import './collections'; -import './toast'; -import './theme'; -// ... all page modules (29 total) -``` - -```typescript -// web/src/collections.ts - Import what YOU use import Alpine from 'alpinejs'; -// Use Alpine for checkbox state -document.addEventListener('alpine:init', () => { - Alpine.data('bookSelection', () => ({ - selectedBooks: new Set(), - toggle(id: string) { /* ... */ } - })); +// Initialize Alpine +window.Alpine = Alpine; +Alpine.start(); + +// Re-export Alpine for other modules to use +export { Alpine }; +``` + +**Add to main.ts**: Append this line at the end of `web/src/main.ts`: + +```typescript +import './alpine'; +``` + +### Step 2: Update TypeScript Files to Register with Alpine + +#### Pattern: Object Registration (Multiple Related Functions) + +**Example**: `web/src/toast.ts` + +**Current** (lines 229-236): +```typescript +(window as any).showToast = { + error: (message: string, duration?: number) => + showToast(message, "error", duration), + success: (message: string, duration?: number) => + showToast(message, "success", duration), + info: (message: string, duration?: number) => + showToast(message, "info", duration), +}; +``` + +**Change to**: +```typescript +import { Alpine } from './alpine'; + +// Register toast API with Alpine +Alpine.global('showToast', { + error: (message: string, duration?: number) => + showToast(message, "error", duration), + success: (message: string, duration?: number) => + showToast(message, "success", duration), + info: (message: string, duration?: number) => + showToast(message, "info", duration), }); ``` -```typescript -// web/src/docs.ts - Import what YOU use -import lunr from 'lunr'; -import hljs from 'highlight.js/lib/common'; -import 'highlight.js/lib/languages/bash'; -import 'highlight.js/lib/languages/go'; -import 'highlight.js/lib/languages/sql'; -import 'highlight.js/lib/languages/http'; -import 'highlight.js/lib/languages/json'; +**Usage in templates**: +```html + + + -// Use lunr for search -const idx = lunr(function() { /* ... */ }); - -// Make hljs available globally (needed for docs template) -if (typeof window !== 'undefined') { - (window as any).hljs = hljs; -} + + + ``` -### ESBuild Handles the Rest +#### Pattern: Namespace Registration (Grouping Related Functions) -When you run `esbuild web/src/main.ts --bundle`: +**Example**: `web/src/api.ts` -1. Starts at `main.ts` -2. Follows all imports through all modules -3. Collects all dependencies -4. Bundles everything into `main.js` -5. Handles deduplication automatically -6. Tree-shakes unused code +**Current** (lines 90-99): +```typescript +(window as any).api = { + get: apiGet, + post: apiPost, + put: apiPut, + delete: apiDelete, + patch: apiPatch, + handleResponse, + handleVoidResponse, + handleError, +}; +``` -**Result**: Single `main.js` with everything you need, nothing you don't. +**Change to**: +```typescript +import { Alpine } from './alpine'; + +Alpine.global('api', { + get: apiGet, + post: apiPost, + put: apiPut, + delete: apiDelete, + patch: apiPatch, + handleResponse, + handleVoidResponse, + handleError, +}); +``` + +**Usage in templates**: +```html + + + + + +``` + +#### Pattern: Stateful Components (Dropdowns, Modals) + +**Example**: `web/src/header.ts` (theme dropdown) + +**Current approach**: Multiple window exports + +**New approach**: Create Alpine component with state + +**Add to `web/src/header.ts`**: +```typescript +import { Alpine } from './alpine'; + +// Register theme dropdown component +Alpine.data('themeDropdown', () => ({ + open: false, + + toggle() { + this.open = !this.open; + }, + + changeTheme(theme: string) { + changeThemeTo(theme); // Reuse existing function + this.open = false; + }, + + init() { + // Load saved theme on init + applyTheme(loadTheme()); + } +})); +``` + +**Usage in templates**: +```html + + + + + +
+ +
+ +
+
+``` + +### Files Requiring Alpine Migration + +**High priority** (used by 10+ templates): +1. `web/src/toast.ts` - Register `showToast` +2. `web/src/api.ts` - Register `api` object +3. `web/src/events.ts` - Register event functions +4. `web/src/storage.ts` - Register storage helpers + +**Medium priority** (stateful components): +5. `web/src/header.ts` - Create `themeDropdown` component +6. `web/src/woodPaneling.ts` - Create `woodPaneling` component +7. `web/src/collections.ts` - Register collection functions +8. `web/src/conflicts.ts` - Register conflict functions +9. `web/src/search.ts` - Register search functions +10. `web/src/dom.ts` - Register DOM helpers + +**Lower priority** (page-specific): +11. `web/src/dashboard.ts` - Dashboard-specific functions +12. `web/src/devices.ts` - Device management functions +13. `web/src/queue.ts` - Queue management functions +14. `web/src/analytics.ts` - Analytics functions (Chart.js) +15. `web/src/admin.ts` - Admin functions + +**Keep as-is for now** (already working): +- Lunr/Highlight.js imports (docs.ts) - already handled +- Chart.js usage - already loaded via CDN --- -## Template Updates +## Phase 3: Update Templates to Alpine Directives -### Current State +### Template Migration Strategy -Templates have multiple script tags: +**151 onclick handlers need migration** across 27 templates. Use this approach: -```html - - - - - - - - - - +1. **Add `x-data` component** to sections with state +2. **Replace `onclick`** with `@click` +3. **Replace `class="hidden"`** with `x-show="!open"` +4. **Add transitions** with `x-transition` +5. **Use `x-model`** for form inputs + +### Template Changes (27 files) + +--- + +#### 1. templates/collections.templ +**File**: `templates/collections.templ` (271 lines) + +**Remove script blocks** (lines 12-13, 111-112): +DELETE all individual ` ``` -### Update to Single Script Tag - -```html - - - - - - - - +**Change to**: +```templ + ``` -### Files to Update +**Update onclick handlers**: -**1. collections.templ** -- Remove `` + +
+``` -**3. All other templates** (25 files) -- Replace all `` +**Line 119** - Back button: +```templ + + + +``` + +**Change to Alpine**: +```templ +
+ + +
+ +
+ + + + + +
+
+
+``` + +**Update user menu** (lines 57-60, 65): +```templ + + +``` + +**Change to**: +```templ +
+ +
+
+``` + +--- + +#### 7. templates/analytics.templ +**File**: `templates/analytics.templ` (100 lines) + +**Remove first script block** (lines 10-14): +```templ + + + + + +``` +DELETE these lines entirely. + +**Note**: Chart.js remains on CDN (intentionally not bundled - 3.4MB, only used on 1 page). + +**Replace analytics.js script** (line 97): +```templ + +``` + +**Change to**: +```templ + + +``` + +--- + +#### 8. templates/bookshelf.templ +**File**: `templates/bookshelf.templ` (266 lines) + +**Remove first script block** (lines 10-13): +```templ + + + + +``` +DELETE these lines entirely. + +**Replace bookshelf.js script** (line 63): +```templ + +``` + +**Change to**: +```templ + +``` + +--- + +#### 9. templates/devices.templ +**File**: `templates/devices.templ` (870 lines) + +**Replace script block** (lines 12-16): +```templ + + + + + +``` + +**Change to**: +```templ + +``` + +--- + +#### 10. templates/index.templ +**File**: `templates/index.templ` (165 lines) + +**Remove first script block** (lines 10-12): +```templ + + + +``` +DELETE these lines entirely. + +**Replace index.js script** (line 138): +```templ + +``` + +**Change to**: +```templ + +``` + +--- + +#### 11. templates/login.templ +**File**: `templates/login.templ` (81 lines) + +**Replace script block** (lines 10-12): +```templ + + + +``` + +**Change to**: +```templ + +``` + +--- + +#### 12. templates/profile.templ +**File**: `templates/profile.templ` (68 lines) + +**Replace script block** (lines 9-11): +```templ + + + +``` + +**Change to**: +```templ + +``` + +--- + +#### 13. templates/queue.templ +**File**: `templates/queue.templ` (169 lines) + +**Remove first script block** (lines 12-15): +```templ + + + + +``` +DELETE these lines entirely. + +**Replace queue.js script** (line 166): +```templ + +``` + +**Change to**: +```templ + +``` + +--- + +#### 14. templates/conflicts.templ +**File**: `templates/conflicts.templ` (239 lines) + +**Remove first script block** (lines 12-15): +```templ + + + + +``` +DELETE these lines entirely. + +**Replace conflicts.js script** (line 234): +```templ + +``` + +**Change to**: +```templ + +``` + +--- + +#### 15. templates/custom_section.templ +**File**: `templates/custom_section.templ` (168 lines) + +**Replace script block** (lines 10-13): +```templ + + + + +``` + +**Change to**: +```templ + +``` + +--- + +#### 16. templates/admin_users.templ +**File**: `templates/admin_users.templ` (145 lines) + +**Replace script block** (lines 9-12): +```templ + + + + +``` + +**Change to**: +```templ + +``` + +--- + +#### 17. templates/collection_rules.templ +**File**: `templates/collection_rules.templ` (420 lines) + +**Replace script block** (lines 10-12): +```templ + + + +``` + +**Change to**: +```templ + +``` + +--- + +#### 18. templates/progress.templ +**File**: `templates/progress.templ` (114 lines) + +**Replace script block** (lines 12-14): +```templ + + + +``` + +**Change to**: +```templ + +``` + +--- + +#### 19. templates/register.templ +**File**: `templates/register.templ` (97 lines) + +**Replace script block** (lines 10-12): +```templ + + + +``` + +**Change to**: +```templ + +``` + +--- + +#### 20. templates/settings.templ +**File**: `templates/settings.templ` (157 lines) + +**Replace script block** (lines 9-11): +```templ + + + +``` + +**Change to**: +```templ + +``` + +--- + +#### 21. templates/stats.templ +**File**: `templates/stats.templ` (162 lines) + +**Replace script block** (lines 9-11): +```templ + + + +``` + +**Change to**: +```templ + +``` + +--- + +#### 22. templates/sync.templ +**File**: `templates/sync.templ` (184 lines) + +**Replace script block** (lines 9-12): +```templ + + + + +``` + +**Change to**: +```templ + +``` + +--- + +#### 23. templates/testing_templ.templ +**File**: `templates/testing_templ.templ` (115 lines) + +**Replace script block** (lines 9-11): +```templ + + + +``` + +**Change to**: +```templ + +``` + +--- + +#### 24. themes/obsidian.templ +**File**: `themes/obsidian.templ` (91 lines) + +**Replace script block** (lines 9-11): +```templ + + + +``` + +**Change to**: +```templ + +``` + +--- + +#### 25. themes/whatsapp.templ +**File**: `themes/whatsapp.templ` (86 lines) + +**Replace script block** (lines 9-11): +```templ + + + +``` + +**Change to**: +```templ + +``` + +--- + +#### 26. themes/midnight.templ +**File**: `themes/midnight.templ` (91 lines) + +**Replace script block** (lines 9-11): +```templ + + + +``` + +**Change to**: +```templ + +``` + +--- + +#### 27. themes/sunset.templ (same pattern as other themes) +**File**: `themes/sunset.templ` (86 lines) + +**Replace script block** (lines 9-11): +```templ + + + +``` + +**Change to**: +```templ + +``` + +--- + +### Remaining Templates (7-26) + +For templates **7-26**, follow this pattern: + +1. **Remove all individual script tags** (usually 2-5 scripts) +2. **Replace with single script**: `` +3. **Replace `onclick` with `@click`** +4. **Add `x-data` for stateful components** (modals, dropdowns) + +**Quick find-replace patterns**: +```bash +# In each template file: +onclick="funcName()" → @click="module.funcName()" +onclick="func('param')" → @click="module.func('param')" +class="hidden" → x-show="!isOpen" (with x-data parent) +``` + +**Common stateful patterns**: + +**Modal pattern**: +```templ + + + + + +
+ +
+ +
+
+``` + +**Confirm delete pattern**: +```templ + + + + + +``` + +**Form submission** (keep HTMX for server calls): +```templ + +
+ + + Name required +
+``` +**File**: `themes/sunset.templ` (86 lines) + +**Replace script block** (lines 9-11): +```templ + + + +``` + +**Change to**: +```templ + +``` + +--- + +## Phase 4: Build and Verify + +### Step 1: Create Alpine Registration File +**File**: `web/src/alpine.ts` (already exists, needs update) + +**Current file** (missing window.Alpine): +```typescript +import Alpine from "alpinejs"; + +// Initialize Alpine +Alpine.start(); + +// Re-export Alpine for other modules to use +export { Alpine }; +``` + +**Change to** (add window.Alpine + type declaration): +```typescript +import Alpine from "alpinejs"; + +// Extend Window interface to include Alpine +declare global { + interface Window { + Alpine: typeof Alpine; + } +} + +// Initialize Alpine +window.Alpine = Alpine; +Alpine.start(); + +// Re-export Alpine for other modules to use +export { Alpine }; +``` + +**Why type declaration**: Fixes TypeScript error "Property 'Alpine' does not exist on type 'Window'" + +**Why `window.Alpine`**: Required for Alpine DevTools browser extension to work. + +### Step 2: Update main.ts +**File**: `web/src/main.ts` + +**Add at end** (after all other imports): +```typescript +import './alpine'; +``` + +### Step 3: Build the Bundle ```bash npm run build:ts ``` -**Expected output**: -- `web/static/main.js` created (~100KB) -- `web/static/main.js.map` created (sourcemap) +**Expected output**: Creates `web/static/main.js` (~350-450KB unminified, ~120-150KB minified with Alpine bundled) + `main.js.map` sourcemap -### Step 3: Verify Build +**Why larger**: Alpine.js adds ~15KB gzipped to bundle (worth it for cleaner code). +### Step 4: Regenerate Templates +```bash +templ generate +``` + +**Expected output**: Regenerates all .templ files with updated Alpine directives + +### Step 5: Verify Alpine is Loaded +```bash +head -n 50 web/static/main.js | grep -i alpine +``` + +**Check for**: Alpine initialization code present in bundle. + +### Step 6: Test in Browser + +1. Start dev server: `go run .` +2. Open browser DevTools (F12) → Console +3. Verify Alpine loaded: + ```javascript + typeof window.Alpine !== 'undefined' // should be true + ``` + +4. Test key pages: + - **Dashboard** (tests theme dropdown, wood paneling) + - **Docs page** (tests lunr search, sidebar toggle) + - **Collections** (tests modal, multiple onclick handlers) + - **Header** (tests theme dropdown, user menu) + - **Devices** (tests multiple modals) + +5. **Check Alpine DevTools** (optional): + - Install Alpine DevTools browser extension + - Inspect `x-data` components in DevTools panel + - Verify reactive state changes + +### Step 7: Verify Functionality + +**In browser console, test Alpine globals**: +```javascript +// Should all be accessible via Alpine +Alpine.stores?.toast // Toast store +Alpine.global('showToast') // Global function +``` + +**Test interactions**: +- Theme dropdown opens/closes smoothly +- Modals open with transitions +- Toast notifications appear +- Forms still submit via HTMX +- Search works on docs page +- All 151 onclick handlers work with `@click` + +### Step 8: Check Bundle Size ```bash -# Check file size ls -lh web/static/main.js - -# Verify bundle contents (should see all modules) -head -20 web/static/main.js - -# Check no 404 errors -grep -r "script src" templates/ | grep -v "main.js" ``` -**Expected**: -- main.js is ~80-120KB (minified) -- No references to htmx.min.js, lunr.min.js, etc. in templates -- Bundle contains all your code + dependencies +**Expected**: ~120-150KB (minified with Alpine included) -### Step 4: Test Application - -```bash -# Restart Go server -podman compose down -podman compose up -d -``` - -**Test pages**: -1. http://localhost:8765/collections - Should load without 404 errors -2. http://localhost:8765/collections/{id} - Test checkbox functionality -3. http://localhost:8765/docs - Search should work, syntax highlighting should work -4. http://localhost:8765/dashboard - Should load normally - -**Check browser console**: -- ✅ No 404 errors for .js files -- ✅ No "f is not a function" or "l is not a function" errors -- ✅ HTMX loaded and working (check `typeof htmx !== 'undefined'`) -- ✅ Alpine.js loaded if used (check `typeof Alpine !== 'undefined'`) -- ✅ Lunr loaded on docs page (check `typeof lunr !== 'undefined'`) -- ✅ Highlight.js loaded on docs page (check `typeof hljs !== 'undefined'`) - -### Step 5: Clean Up (Optional) - -After confirming everything works: - -```bash -cd /home/nymusicman/Code/bookhoard/web/static - -# Remove old separate .js files -rm admin.js analytics.js api-explorer.js api.js bookshelf.js -rm collections.js conflicts.js custom-section-builder.js dashboard.js -rm device-management.js dom.js events.js header.js library.js -rm linking.js password_validation.js queue.js search.js storage.js -rm theme.js themeDropdown.js toast.js woodPaneling.js woodPanelingInit.js -``` - -**Keep**: -- `main.js` (new bundle) -- `main.js.map` (sourcemap) -- `style.css` (Tailwind output) -- `input.css` (Tailwind input) -- `placeholder-book.svg` (asset) -- `highlight-dark.min.css` (CSS only) +**Breakdown**: +- App code: ~100KB +- Alpine.js: ~15KB gzipped +- Lunr: ~10KB gzipped +- Highlight.js: ~5KB gzipped +- **Total**: ~130KB / ~40KB gzipped --- -## Verification Checklist +## Phase 5: Migrate TypeScript Files to Alpine (Step-by-Step) -### Before Declaring Complete +### Priority 1: Core Utilities (Do These First) -- [ ] `npm run build:ts` completes without errors -- [ ] `web/static/main.js` exists (~80-120KB) -- [ ] `web/static/main.js.map` exists (sourcemap) -- [ ] No 404 errors in browser console for .js files -- [ ] HTMX working (check network tab for AJAX requests) -- [ ] Docs page search working -- [ ] Docs page syntax highlighting working -- [ ] Collections page loads correctly -- [ ] No `(window as any)` type casts in TypeScript files (unless for SSR data) -- [ ] All npm imports are in files that use them (not main.ts) -- [ ] `main.ts` only imports page modules (not dependencies) -- [ ] Templates updated to use single `` -2. Only import highlight.js languages you use: +### htmx.org +**File**: Loaded via separate script tag in base templates +**Reason**: Core framework, needs to load before main.js +**Approach**: Keep as separate script tag (not bundled), works great with Alpine + +--- + +## Architecture Notes + +### Why Alpine.js Over Window Exports? +**Alpine is the modern approach** for this codebase: +1. **Reactive state management**: `x-data`, `x-show`, `x-model` instead of manual DOM manipulation +2. **Clean templates**: `@click` instead of `onclick="window.func()"` +3. **Component-based**: `Alpine.data()` for reusable components +4. **SSR-friendly**: Works with progressive enhancement +5. **No global pollution**: Functions registered in Alpine scope, not window +6. **Better DX**: Alpine DevTools for debugging reactive state + +### Hybrid Approach: Alpine + HTMX +- **Alpine**: Client-side state (modals, dropdowns, theme, form validation) +- **HTMX**: Server calls (form submissions, API calls, data fetching) +- **Why both**: HTMX excels at server communication, Alpine excels at client-side state + +### ESBuild + Alpine Benefits +- **Single bundle**: Alpine bundled with app code (~15KB gzipped) +- **No CDN dependency**: Faster load, no network request for Alpine +- **Tree-shaking**: Unused Alpine features not included +- **ES2020 target**: Works on Chrome 80+, Firefox 72+, Safari 13.1+ + +### Migration Path: Window → Alpine +**Old approach**: ```typescript -// DON'T: import 'highlight.js'; // All 190+ languages = 5.4MB - -// DO: import only what you use -import hljs from 'highlight.js/lib/common'; -import 'highlight.js/lib/languages/bash'; -import 'highlight.js/lib/languages/go'; -import 'highlight.js/lib/languages/sql'; -import 'highlight.js/lib/languages/http'; -import 'highlight.js/lib/languages/json'; +(window as any).showToast = showToast; +``` +```html + ``` -3. Check for duplicate imports (ESBuild should dedupe, but verify) +**New approach**: +```typescript +Alpine.global('showToast', showToast); +``` +```html + +``` + +### Why Not Put Everything in main.ts? +Main.ts stays simple (24 lines) because: +- **Imports where used**: docs.ts imports lunr, not main.ts +- **Tree-shaking**: Only what's used gets bundled +- **Maintainability**: Each file has its own dependencies +- **Testing**: Can test individual modules independently +- **Alpine registration**: Each file registers its own Alpine globals/components --- -## Summary +## Success Criteria -### What We're Doing - -1. ✅ Setup ESBuild with `--target=es2020` (broad browser support) -2. ✅ Bundle from `web/src/main.ts` (single entry point) -3. ✅ Output to `web/static/main.js` (single bundle) -4. ✅ Import dependencies where used (not in main.ts) -5. ✅ Remove `(window as any)` type casts (direct imports) -6. ✅ Update templates to use single script tag -7. ✅ Test thoroughly (no regressions) - -### Expected Bundle Size - -- Your code: ~8-10KB minified -- HTMX: ~15KB -- Alpine.js: ~15KB -- Lunr: ~10KB -- Highlight.js (5 languages): ~50KB (not 5.4MB!) -- **Total: ~100KB** (with gzip: ~30KB) - -### Browser Support - -ES2020 supports: -- Chrome 80+ (Feb 2020) -- Firefox 72+ (Jan 2020) -- Safari 13.1+ (Mar 2020) -- Edge 80+ (Jan 2020) -- Lightweight browsers (Ephiphany, Falkon) from 2020+ - -Covers 99%+ of real users, including: -- Ubuntu 22.04 LTS users (supported until 2027) -- Debian 12 users -- Fedora 38+ users -- Arch Linux users (rolling release) - -### Next Steps - -1. Update `package.json` build scripts -2. Create `web/src/search-data.ts` (if using Option C for SSR data) -3. Add imports to individual .ts files where needed -4. Update all templates to use single script tag -5. Run `npm run build:ts` -6. Test thoroughly -7. Clean up old .js files +✅ 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 --- -## References +## Post-Migration: Development Workflow -- **ESBuild docs**: https://esbuild.github.io/ -- **PROJECT_GUIDELINES.md**: Project conventions and protocols -- **TailwindCSS docs**: https://tailwindcss.com/docs -- **HTMX docs**: https://htmx.org/docs/ -- **Alpine.js docs**: https://alpinejs.dev/ -- **Lunr.js docs**: https://lunrjs.com/ -- **Highlight.js docs**: https://highlightjs.org/ +### Watch Mode (Development) +```bash +npm run dev +``` +Watches both TypeScript and templates, rebuilds on changes. + +### Production Build +```bash +npm run build:ts +templ generate +go build +``` +Creates minified bundle, regenerates templates, builds Go binary. + +### Debugging +Use `main.js.map` sourcemap in browser DevTools to debug original TypeScript sources. + +--- + +## Files Modified Summary + +### New Files Created +- `web/src/alpine.ts` (new) - Alpine initialization and exports + +### Configuration Files +- ✅ package.json (already correct - no changes needed) +- ✅ tsconfig.json (already ES2020 - no changes needed) + +### Source Files Modified +- `web/src/main.ts` (add `import './alpine'` at end) +- `web/src/docs.ts` (remove 4 window globals, add Alpine registration) +- `web/src/toast.ts` (replace window export with Alpine.global) +- `web/src/api.ts` (replace window export with Alpine.global) +- `web/src/storage.ts` (replace window export with Alpine.global) +- `web/src/events.ts` (replace window export with Alpine.global) +- `web/src/header.ts` (add Alpine.data themeDropdown component) +- `web/src/woodPaneling.ts` (add Alpine.data component) +- `web/src/collections.ts` (register namespace with Alpine.global) +- `web/src/conflicts.ts` (register namespace with Alpine.global) +- `web/src/devices.ts` (register namespace with Alpine.global) +- `web/src/queue.ts` (register namespace with Alpine.global) +- `web/src/search.ts` (register namespace with Alpine.global) +- `web/src/dom.ts` (register namespace with Alpine.global) +- Plus 10+ more TypeScript files (register functions with Alpine) + +### Template Files Modified (27 files) +All templates updated to: +1. Remove individual script tags +2. Use single `` +3. Replace `onclick` with `@click` +4. Add `x-data` for stateful components (modals, dropdowns) + +Templates: +1. templates/collections.templ (migrate 7 onclick handlers) +2. templates/docs.templ (migrate sidebar toggle to Alpine) +3. templates/admin.templ +4. templates/admin_library.templ +5. templates/dashboard.templ (theme dropdown, wood paneling) +6. templates/header.templ (theme dropdown, user menu - KEY FILE) +7. templates/analytics.templ (keep Chart.js CDN) +8. templates/bookshelf.templ +9. templates/devices.templ (multiple modals) +10. templates/index.templ +11. templates/login.templ +12. templates/profile.templ +13. templates/queue.templ +14. templates/conflicts.templ +15. templates/custom_section.templ +16. templates/admin_users.templ +17. templates/collection_rules.templ +18. templates/progress.templ +19. templates/register.templ +20. templates/settings.templ +21. templates/stats.templ +22. templates/sync.templ +23. templates/testing_templ.templ +24. themes/obsidian.templ +25. themes/whatsapp.templ +26. themes/midnight.templ +27. themes/sunset.templ + +### Generated Files +- `web/static/main.js` (new bundled output with Alpine) +- `web/static/main.js.map` (new sourcemap) + +### Files to Delete (After Verification) +All individual .js files in `web/static/` (except main.js and main.js.map): +- api.js, events.js, dom.js, toast.js, storage.js +- theme.js, header.js, themeDropdown.js, woodPaneling.js +- docs.js, search.js, collections.js, conflicts.js +- dashboard.js, admin*.js, analytics.js, bookshelf.js +- devices.js, index.js, login.js, profile.js, queue.js +- custom_section.js, progress.js, register.js, settings.js +- stats.js, sync.js, testing_templ.js +- obsidian.js, whatsapp.js, midnight.js, sunset.js + +--- + +## Migration Checklist + +Use this checklist to track progress: + +### Phase 1: Clean Up docs.ts +- [ ] Add imports for lunr and hljs (lines 3-4) +- [ ] Remove `(window as any).lunr` check (line 50) +- [ ] Replace `(window as any).lunrIndex` with `lunr.Builder.loadJs` (line 56) +- [ ] Replace `(window as any).docsData` with `docs` import (line 73) +- [ ] Replace window export with Alpine registration (line 99) + +### Phase 2: Create Alpine Infrastructure +- [ ] Create `web/src/alpine.ts` with Alpine initialization +- [ ] Add `import './alpine'` to `web/src/main.ts` +- [ ] Build and verify Alpine is loaded (`npm run build:ts`) + +### Phase 3: Migrate Core TypeScript Files (Priority 1) +- [ ] `web/src/toast.ts` - Register with Alpine.global +- [ ] `web/src/api.ts` - Register with Alpine.global +- [ ] `web/src/storage.ts` - Register with Alpine.global +- [ ] `web/src/events.ts` - Register with Alpine.global +- [ ] `web/src/dom.ts` - Register with Alpine.global + +### Phase 4: Migrate Stateful Components (Priority 2) +- [ ] `web/src/header.ts` - Create themeDropdown Alpine.data component +- [ ] `web/src/woodPaneling.ts` - Create woodPaneling Alpine.data component +- [ ] Test theme dropdown in browser +- [ ] Test wood paneling in browser + +### Phase 5: Migrate Page-Specific Files (Priority 3) +- [ ] `web/src/collections.ts` - Register namespace +- [ ] `web/src/devices.ts` - Register namespace +- [ ] `web/src/queue.ts` - Register namespace +- [ ] `web/src/conflicts.ts` - Register namespace +- [ ] `web/src/search.ts` - Register namespace + +### Phase 6: Update Templates (27 files) +- [ ] templates/header.templ (CRITICAL - theme dropdown) +- [ ] templates/docs.templ (sidebar toggle) +- [ ] templates/dashboard.templ (wood paneling) +- [ ] templates/collections.templ (7 onclick handlers) +- [ ] templates/admin.templ through themes/sunset.templ (23 remaining) +- [ ] Run `templ generate` after template changes + +### Phase 7: Test and Verify +- [ ] Build: `npm run build:ts` +- [ ] Check bundle size: ~130KB minified +- [ ] Start dev server: `go run .` +- [ ] Test all 151 onclick handlers +- [ ] Verify Alpine DevTools shows components +- [ ] Test HTMX forms still work +- [ ] Check browser console for errors +- [ ] Test on Chrome, Firefox, Safari (ES2020 compatibility) + +### Phase 8: Clean Up +- [ ] Delete all individual .js files from web/static/ +- [ ] Commit changes +- [ ] Deploy and test in production + +--- + +## Additional Resources + +### Alpine.js Documentation +- Official docs: https://alpinejs.dev/ +- Essentials guide: https://alpinejs.dev/essentials/start +- `x-data` docs: https://alpinejs.edu/directives/data +- `@click` docs: https://alpinejs.edu/directives/on +- `x-show` docs: https://alpinejs.edu/directives/show +- `x-transition` docs: https://alpinejs.edu/directives/transition + +### Alpine + HTMX Integration +- Blog post: https://htmx.org/examples/blog/ +- Both work together seamlessly - Alpine for client state, HTMX for server calls + +### ESBuild Documentation +- Official docs: https://esbuild.github.io/ +- API: https://esbuild.github.io/api/ +- Bundling: https://esbuild.github.io/api/#bundle + +### Migration Tips +1. **Start small**: Migrate 1-2 files at a time, test frequently +2. **Use Alpine DevTools**: Install browser extension for debugging +3. **Keep HTMX for forms**: Don't replace `hx-post` with Alpine fetch +4. **Test in browser**: Console errors will show missing Alpine globals +5. **Progressive migration**: Can use window exports AND Alpine during transition + +### Configuration +- ✅ package.json (already correct) +- ✅ tsconfig.json (already ES2020) + +### Source Files +- `web/src/docs.ts` (add imports, remove 4 window globals) + +### Template Files (27 files) +All templates updated to use single `` tag: + +1. templates/collections.templ +2. templates/docs.templ +3. templates/admin.templ +4. templates/admin_library.templ +5. templates/dashboard.templ +6. templates/header.templ +7. templates/analytics.templ +8. templates/bookshelf.templ +9. templates/devices.templ +10. templates/index.templ +11. templates/login.templ +12. templates/profile.templ +13. templates/queue.templ +14. templates/conflicts.templ +15. templates/custom_section.templ +16. templates/admin_users.templ +17. templates/collection_rules.templ +18. templates/progress.templ +19. templates/register.templ +20. templates/settings.templ +21. templates/stats.templ +22. templates/sync.templ +23. templates/testing_templ.templ +24. themes/obsidian.templ +25. themes/whatsapp.templ +26. themes/midnight.templ +27. themes/sunset.templ + +### Generated Files +- `web/static/main.js` (new bundled output) +- `web/static/main.js.map` (new sourcemap) + +### Files to Delete (After Verification) +All individual .js files in `web/static/` (except main.js and main.js.map) + +--- + +## Summary: What We're Achieving + +### The Problem +- 151 inline `onclick` handlers using `window.funcName()` +- 20+ TypeScript files exporting to `(window as any)` +- 27 template files with 3-5 script tags each +- Manual DOM manipulation for state (modals, dropdowns) +- No reactive state management +- Global namespace pollution + +### The Solution +1. **Bundle with ESBuild**: Single ~130KB minified bundle (ES2020 target) +2. **Alpine.js for state**: Reactive components with `x-data`, `@click`, `x-show` +3. **Keep HTMX for forms**: Server-side communication remains unchanged +4. **Modern patterns**: No more `(window as any)`, clean templates + +### Benefits +- ✅ **Performance**: Single HTTP request for all JS, better caching +- ✅ **Maintainability**: Alpine components vs imperative DOM manipulation +- ✅ **Type safety**: No more `window as any` hacks +- ✅ **Developer experience**: Clean templates, Alpine DevTools, sourcemaps +- ✅ **User experience**: Smooth transitions, reactive UI, faster loads +- ✅ **Bundle size**: Only ~40KB gzipped (was 30+ HTTP requests before) + +### Key Architecture Decisions +1. **Alpine over vanilla event listeners**: Reactive state is cleaner +2. **Alpine over React/Vue**: Lightweight, SSR-friendly, works with HTMX +3. **Bundle Alpine**: No CDN dependency, faster load, tree-shaking +4. **Keep HTMX**: Don't fix what works - HTMX excels at server communication +5. **Progressive migration**: Migrate incrementally, can mix old/new during transition + +### Migration Effort +- **TypeScript files**: 20+ files (5-10 min each) = ~3 hours +- **Templates**: 27 files (10-15 min each) = ~5 hours +- **Testing**: 2-3 hours +- **Total**: ~10-12 hours for complete migration + +### Maintenance Going Forward +- Adding new feature? Create Alpine component or register function +- New page? Single `