diff --git a/esbuild-setup.md b/esbuild-setup.md new file mode 100644 index 0000000..2f5772a --- /dev/null +++ b/esbuild-setup.md @@ -0,0 +1,719 @@ +# ESBuild Setup and Migration Guide + +## Overview + +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. + +--- + +## Table of Contents + +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) + +--- + +## 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 + +```json +"--target=es2020" +``` + +**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 + +--- + +## package.json Setup + +### Current State (Already Correct!) + +Your `package.json` already has perfect dependency classification: + +```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" + } +} +``` + +**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` + +```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; +``` + +### 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 + +```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; +} +``` + +#### 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) + +```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]; +``` + +**Option B: Data attributes** (cleaner, requires template changes) + +```go +// In Go template +
+``` + +```typescript +// In TypeScript +const container = document.getElementById('search-container')!; +const searchIndex = JSON.parse(container.dataset.searchIndex!); +const docsData = JSON.parse(container.dataset.docsData!); +``` + +**Option C: Loader module** (best, most maintainable) + +```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 }; +``` + +```typescript +// web/src/docs.ts +import { lunrIndex, docsData, loadSearchData } from './search-data'; + +document.addEventListener('DOMContentLoaded', async () => { + await loadSearchData(); + initializeDocsSearch(); +}); + +function performDocsSearch(query: string) { + // Use imported data, no globals + const idx = lunrIndex; + const doc = docsData[result.ref]; +} +``` + +#### 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` + +```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(); +}); +``` + +```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` + +--- + +## Import Strategy + +### Principle: Import Where Used + +**❌ DON'T**: Import everything in main.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) { /* ... */ } + })); +}); +``` + +```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'; + +// 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 + +When you run `esbuild web/src/main.ts --bundle`: + +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 + +**Result**: Single `main.js` with everything you need, nothing you don't. + +--- + +## Template Updates + +### Current State + +Templates have multiple script tags: + +```html + + + + + + + + + + +``` + +### Update to Single Script Tag + +```html + + + + + + + + +``` + +### Files to Update + +**1. collections.templ** +- Remove `` + +**3. All other templates** (25 files) +- Replace all `` + +### Pattern + +**Before**: +```html + + + + +``` + +**After**: +```html + +``` + +--- + +## Build and Test + +### Step 1: Install Dependencies + +```bash +cd /home/nymusicman/Code/bookhoard +npm install +``` + +**Expected output**: +- `node_modules/htmx.org/` exists +- `node_modules/alpinejs/` exists +- `node_modules/lunr/` exists +- `node_modules/highlight.js/` exists + +### Step 2: Build Bundle + +```bash +npm run build:ts +``` + +**Expected output**: +- `web/static/main.js` created (~100KB) +- `web/static/main.js.map` created (sourcemap) + +### Step 3: Verify Build + +```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 + +### 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) + +--- + +## Verification Checklist + +### Before Declaring Complete + +- [ ] `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 `