Updated ESBUILD_MIGRATION_PLAN.md to reflect: - Phase 1 completion status - New Alpine.js conversion work - Remaining templates status (docs.templ excluded per user request)
2041 lines
57 KiB
Markdown
2041 lines
57 KiB
Markdown
# ESBuild Migration Plan: TypeScript → ES Modules + Alpine
|
|
|
|
## Executive Summary
|
|
|
|
**Goal**: Migrate from individual JavaScript files with window globals to a single bundled `main.js` using ESBuild, ES modules for internal dependencies, and Alpine.js for template interactivity.
|
|
|
|
**Architecture Principles**:
|
|
1. **ES Modules** for all TypeScript → TypeScript dependencies
|
|
2. **Alpine.js** ONLY as a bridge to templates (onclick → @click)
|
|
3. **SSR-first** - Server renders complete HTML, client-side JS only for interactivity
|
|
4. **No window globals** - everything through proper imports/exports
|
|
5. **Incremental migration** - Each phase is complete and testable
|
|
|
|
**Target Bundle**: ~100-130KB minified (ES2020 target = Chrome 80+, Firefox 72+, Safari 13.1+, Edge 80+)
|
|
|
|
---
|
|
|
|
## Current State Analysis
|
|
|
|
### Problems with Current Architecture
|
|
|
|
1. **274 window global references** across 21 TypeScript files
|
|
2. **193+ internal TypeScript dependencies** via window (should be ES imports)
|
|
3. **27 template files** with 150+ unique onclick handlers
|
|
4. **Function wrapping** (themeDropdown.ts wraps header.ts functions)
|
|
5. **Individual .js files** loaded in each template (not bundled)
|
|
6. **Mixed patterns** - some files have ES exports, some don't
|
|
|
|
### Files Requiring Migration
|
|
|
|
**TypeScript Source Files** (21 files):
|
|
- api.ts, toast.ts, storage.ts, events.ts, dom.ts
|
|
- theme.ts, header.ts, themeDropdown.ts, woodPaneling.ts
|
|
- library.ts, collections.ts, conflicts.ts, queue.ts
|
|
- dashboard.ts, admin.ts, bookshelf.ts, search.ts
|
|
- device-management.ts, analytics.ts, custom-section-builder.ts
|
|
- docs.ts, api-explorer.ts, password_validation.ts
|
|
|
|
**Template Files** (27 files):
|
|
- dashboard.templ, collections.templ, header.templ, admin.templ
|
|
- docs.templ, bookshelf.templ, devices.templ, queue.templ
|
|
- conflicts.templ, analytics.templ, login.templ, register.templ
|
|
- profile.templ, settings.templ, stats.templ, sync.templ
|
|
- progress.templ, custom_section.templ, collection_rules.templ
|
|
- admin_library.templ, admin_users.templ, api_explorer.templ
|
|
- Plus 8 more modal/component templates
|
|
|
|
---
|
|
|
|
## Architecture Principles
|
|
|
|
### 1. ES Module Exports (TypeScript → TypeScript)
|
|
|
|
Every utility module exports functions for other TypeScript files to import:
|
|
|
|
```typescript
|
|
// api.ts
|
|
export async function apiGet(url: string): Promise<Response> { ... }
|
|
export async function apiPost(url: string, data?: unknown): Promise<Response> { ... }
|
|
export async function handleResponse<T>(response: Response): Promise<T> { ... }
|
|
```
|
|
|
|
```typescript
|
|
// library.ts (consumes api.ts)
|
|
import { apiGet, handleResponse } from "./api";
|
|
|
|
const response = await apiGet("/libraries");
|
|
const result = await handleResponse<LibrariesResponse>(response);
|
|
```
|
|
|
|
### 2. Alpine.js Bridge (Templates → TypeScript)
|
|
|
|
Alpine is ONLY used to expose functions to templates, not for internal TS dependencies:
|
|
|
|
```typescript
|
|
// api.ts (bottom of file)
|
|
import { Alpine } from "./alpine";
|
|
|
|
Alpine.global("api", {
|
|
get: apiGet,
|
|
post: apiPost,
|
|
put: apiPut,
|
|
delete: apiDelete,
|
|
handleResponse,
|
|
handleVoidResponse,
|
|
handleError,
|
|
});
|
|
```
|
|
|
|
Template usage:
|
|
```html
|
|
<!-- Before -->
|
|
<button onclick="apiPost('/api/save', data)">Save</button>
|
|
|
|
<!-- After -->
|
|
<button @click="api.post('/api/save', data)">Save</button>
|
|
```
|
|
|
|
### 3. SSR-First with Progressive Enhancement
|
|
|
|
- Server renders complete HTML with data
|
|
- Client-side JavaScript only for interactivity (modals, dropdowns, forms)
|
|
- Data loaded via fetch() APIs (like docs search index)
|
|
- No window globals for data injection
|
|
|
|
---
|
|
|
|
## Migration Strategy: Incremental with No Legacy Code
|
|
|
|
### Why This Strategy Works
|
|
|
|
1. **No breaking changes during migration** - App works at every phase
|
|
2. **Can ship anytime** - Partial migrations are functional
|
|
3. **No legacy code** - Each file completely migrated, no half-states
|
|
4. **Easy rollback** - If issues arise, revert individual files/templates
|
|
5. **Testable at each step** - Clear verification criteria
|
|
|
|
---
|
|
|
|
## Phase 0: Foundation - Add ES Module Exports
|
|
|
|
**Goal**: Add proper ES exports to all utility modules WITHOUT breaking existing functionality.
|
|
|
|
**Files**: 8 utility modules
|
|
|
|
**Duration**: 1-2 hours
|
|
|
|
### Step 0.1: Add Exports to Utility Modules
|
|
|
|
For each file, add `export` statements while keeping window exports temporarily:
|
|
|
|
**web/src/api.ts** (Already has Alpine, just needs ES exports)
|
|
```typescript
|
|
// Add at bottom (after Alpine.global block):
|
|
export { apiGet, apiPost, apiPut, apiDelete, apiPatch, handleResponse, handleVoidResponse, handleError };
|
|
```
|
|
|
|
**web/src/toast.ts** (Already has Alpine, just needs ES exports)
|
|
```typescript
|
|
// Add at bottom (after Alpine.global block):
|
|
export { showToast };
|
|
export type { ToastType };
|
|
```
|
|
|
|
**web/src/storage.ts** (Already has exports! ✅)
|
|
```typescript
|
|
// Already has:
|
|
// export { getToken, setToken, ... };
|
|
// No changes needed!
|
|
```
|
|
|
|
**web/src/events.ts**
|
|
```typescript
|
|
// Add at bottom:
|
|
export {
|
|
onDelegatedClick,
|
|
onDelegatedSubmit,
|
|
onDelegatedChange,
|
|
onDelegatedKeydown,
|
|
getDataAttribute,
|
|
setDataAttribute,
|
|
onClick,
|
|
onSubmit,
|
|
onChange,
|
|
onKeydown,
|
|
onInput,
|
|
preventDefault,
|
|
stopPropagation,
|
|
};
|
|
```
|
|
|
|
**web/src/dom.ts** (Already has exports! ✅)
|
|
```typescript
|
|
// Already has:
|
|
// export { escapeHtml, querySelector, ... };
|
|
// No changes needed!
|
|
```
|
|
|
|
**web/src/theme.ts**
|
|
```typescript
|
|
// Add at bottom:
|
|
export { applyTheme, loadTheme, changeTheme, loadUserTheme, initializeTheme };
|
|
export type { ThemeType };
|
|
```
|
|
|
|
**web/src/header.ts**
|
|
```typescript
|
|
// Add at bottom:
|
|
export { toggleThemeDropdown, toggleUserMenu, changeThemeTo, logout };
|
|
```
|
|
|
|
**web/src/woodPaneling.ts**
|
|
```typescript
|
|
// Add at bottom:
|
|
export { changeWoodPaneling, loadWoodPaneling, updateWoodPanelingIndicators };
|
|
```
|
|
|
|
### Step 0.2: Verify Build
|
|
|
|
```bash
|
|
npm run build:ts
|
|
```
|
|
|
|
**Expected**: Build succeeds, no errors
|
|
|
|
**Why this works**: We're adding exports without changing anything else. App still uses window globals, so functionality is unchanged.
|
|
|
|
---
|
|
|
|
## Phase 1: Internal TypeScript Dependencies
|
|
|
|
**Goal**: Convert all internal `window.XXX` reads to proper ES module imports.
|
|
|
|
**Files**: 13 consumer files
|
|
|
|
**Duration**: 4-6 hours
|
|
|
|
### 1.1: Simple Consumers (Low Risk)
|
|
|
|
Files with minimal window dependencies:
|
|
|
|
**web/src/dashboard.ts**
|
|
```typescript
|
|
// Add at top:
|
|
import { apiGet, apiPost, apiPut, apiDelete, apiPatch, handleResponse, handleVoidResponse, handleError } from "./api";
|
|
import { showToast } from "./toast";
|
|
|
|
// Replace ALL instances:
|
|
// Line 22: (window as any).showToast.error("msg") → showToast("msg", "error")
|
|
// Line 37: (window as any).showToast.error("msg") → showToast("msg", "error")
|
|
// Line 128: (window as any).api.put → apiPut
|
|
// Line 135: (window as any).showToast.success("msg") → showToast("msg", "success")
|
|
// ... (10 more replacements)
|
|
```
|
|
|
|
**web/src/search.ts**
|
|
```typescript
|
|
// No changes needed! ✅
|
|
// Already uses localStorage directly, doesn't read from window
|
|
// Only exports selectLibraryAndBook
|
|
```
|
|
|
|
**web/src/device-management.ts**
|
|
```typescript
|
|
// Add at top:
|
|
import { showToast } from "./toast";
|
|
|
|
// Replace ALL instances:
|
|
// Lines 32-34, 39-42, 78-82, 89-92: const toast = (window as any).showToast → showToast
|
|
```
|
|
|
|
**web/src/password_validation.ts**
|
|
```typescript
|
|
// No window reads found, only exports initPasswordValidation
|
|
// No changes needed
|
|
```
|
|
|
|
### 1.2: Moderate Consumers
|
|
|
|
Files with multiple window dependencies:
|
|
|
|
**web/src/admin.ts**
|
|
```typescript
|
|
// Add at top:
|
|
import { showToast } from "./toast";
|
|
|
|
// Replace ALL instances (14 occurrences):
|
|
// Lines 12-13, 17-18, 23-24, 40-41, 45-46, 53-54, 119-120, 152-153, 161-162
|
|
// (window as any).showToast.success("msg") → showToast("msg", "success")
|
|
// (window as any).showToast.error → showToast.error
|
|
```
|
|
|
|
**web/src/queue.ts**
|
|
```typescript
|
|
// Add at top:
|
|
import { showToast } from "./toast";
|
|
|
|
// Replace ALL instances (8 occurrences):
|
|
// Lines 30-32, 37-39, 56-58, 63-65, 82-84, 89-91
|
|
```
|
|
|
|
**web/src/conflicts.ts**
|
|
```typescript
|
|
// Add at top:
|
|
import { showToast } from "./toast";
|
|
|
|
// Replace ALL instances (6 occurrences):
|
|
// Lines 39-41, 45-48, 53-55, 78-80, 85-87
|
|
```
|
|
|
|
**web/src/linking.ts**
|
|
```typescript
|
|
// Add at top:
|
|
import { showToast } from "./toast";
|
|
|
|
// Replace ALL instances:
|
|
// (window as any).showToast.success/error("msg") → showToast("msg", "success"/"error")
|
|
```
|
|
|
|
**web/src/custom-section-builder.ts**
|
|
```typescript
|
|
// Add at top:
|
|
import { showToast } from "./toast";
|
|
import { apiGet, apiPost, apiPut, apiDelete, apiPatch, handleResponse, handleVoidResponse, handleError } from "./api";
|
|
|
|
// Replace ALL instances (9 occurrences):
|
|
// Lines 7, 10, 13, 16, 19, 22, 25, 28
|
|
```
|
|
|
|
**web/src/analytics.ts**
|
|
```typescript
|
|
// Add at top:
|
|
import { apiGet, apiPost, apiPut, apiDelete, apiPatch, handleResponse, handleVoidResponse, handleError } from "./api";
|
|
import { querySelector, querySelectorAll, getElementById, createElement, showElement, hideElement, toggleElement, addClass, removeClass, toggleClass, hasClass, setTextContent, setInnerHTML, escapeHtml } from "./dom";
|
|
|
|
// Replace ALL instances:
|
|
// (window as any).api.get → apiGet
|
|
// (window as any).dom.getElementById → getElementById
|
|
```
|
|
|
|
**web/src/bookshelf.ts**
|
|
```typescript
|
|
// Add at top:
|
|
import { showToast } from "./toast";
|
|
import { apiGet, apiPost, apiPut, apiDelete, apiPatch, handleResponse, handleVoidResponse, handleError } from "./api";
|
|
import { onDelegatedClick, onDelegatedSubmit, onDelegatedChange, onDelegatedKeydown, getDataAttribute, setDataAttribute, onClick, onSubmit, onChange, onKeydown, onInput, preventDefault, stopPropagation } from "./events";
|
|
|
|
// Replace ALL instances
|
|
```
|
|
|
|
**web/src/api-explorer.ts**
|
|
```typescript
|
|
// Add at top:
|
|
import { apiGet, apiPost, apiPut, apiDelete, apiPatch, handleResponse, handleVoidResponse, handleError } from "./api";
|
|
import { querySelector, querySelectorAll, getElementById, createElement, showElement, hideElement, toggleElement, addClass, removeClass, toggleClass, hasClass, setTextContent, setInnerHTML, escapeHtml } from "./dom";
|
|
|
|
// Replace ALL instances
|
|
```
|
|
|
|
### 1.3: Complex Consumers (High Risk)
|
|
|
|
Files with heavy window dependencies:
|
|
|
|
**web/src/library.ts** (55 occurrences!)
|
|
```typescript
|
|
// Add at top:
|
|
import { apiGet, apiPost, apiPut, apiDelete, apiPatch, handleResponse, handleVoidResponse, handleError } from "./api";
|
|
import { showToast } from "./toast";
|
|
import { querySelector, querySelectorAll, getElementById, createElement, showElement, hideElement, toggleElement, addClass, removeClass, toggleClass, hasClass, setTextContent, setInnerHTML, escapeHtml } from "./dom";
|
|
|
|
// Systematic replacement:
|
|
// (window as any).api.get → apiGet
|
|
// (window as any).api.handleResponse → handleResponse
|
|
// (window as any).api.handleError → handleError
|
|
// (window as any).showToast.success("msg") → showToast("msg", "success")
|
|
// (window as any).dom.createElement → createElement
|
|
```
|
|
|
|
**web/src/collections.ts** (54 occurrences!)
|
|
```typescript
|
|
// Add at top:
|
|
import { apiGet, apiPost, apiPut, apiDelete, apiPatch, handleResponse, handleVoidResponse, handleError } from "./api";
|
|
import { showToast } from "./toast";
|
|
import { querySelector, querySelectorAll, getElementById, createElement, showElement, hideElement, toggleElement, addClass, removeClass, toggleClass, hasClass, setTextContent, setInnerHTML, escapeHtml } from "./dom";
|
|
|
|
// Systematic replacement of all 54 instances
|
|
```
|
|
|
|
### 1.4: Restructure Function Wrapping
|
|
|
|
This section eliminates the anti-pattern of wrapping functions from other files. Currently, `themeDropdown.ts` overrides `header.ts` functions by reading from window and reassigning them. We'll convert this to proper ES module composition.
|
|
|
|
---
|
|
|
|
#### 1.4.1: web/src/themeDropdown.ts - Complete Rewrite
|
|
|
|
**Current Problem (Lines 1-32):**
|
|
```typescript
|
|
// Anti-pattern: Reading and reassigning window globals
|
|
const originalToggleThemeDropdown = (window as any).toggleThemeDropdown;
|
|
(window as any).toggleThemeDropdown = () => {
|
|
originalToggleThemeDropdown();
|
|
updateThemeIndicators();
|
|
(window as any).updateWoodPanelingIndicators?.();
|
|
};
|
|
```
|
|
|
|
**Step-by-Step Conversion:**
|
|
|
|
**1. Add ES Module Imports (at top of file, after line 1):**
|
|
```typescript
|
|
import { toggleThemeDropdown } from "./header";
|
|
import { updateWoodPanelingIndicators } from "./woodPaneling";
|
|
```
|
|
|
|
**2. Remove window read/override (delete lines 1-8):**
|
|
```typescript
|
|
// DELETE THESE LINES:
|
|
// const originalToggleThemeDropdown = (window as any).toggleThemeDropdown;
|
|
// (window as any).toggleThemeDropdown = () => {
|
|
// originalToggleThemeDropdown();
|
|
// updateThemeIndicators();
|
|
// (window as any).updateWoodPanelingIndicators?.();
|
|
// };
|
|
```
|
|
|
|
**3. Create new composed function (replace deleted code with):**
|
|
```typescript
|
|
// Compose the theme dropdown behavior
|
|
export function initializeThemeDropdown(): void {
|
|
// Call original function from header.ts
|
|
toggleThemeDropdown();
|
|
// Then update theme indicators
|
|
updateThemeIndicators();
|
|
// Update wood paneling indicators
|
|
updateWoodPanelingIndicators();
|
|
}
|
|
```
|
|
|
|
**4. Keep existing updateThemeIndicators function (no change needed)**
|
|
|
|
**5. Add Alpine registration at bottom of file (after existing code):**
|
|
```typescript
|
|
import { Alpine } from "./alpine";
|
|
|
|
// Register composed function with Alpine
|
|
Alpine.global("themeDropdown", {
|
|
initialize: initializeThemeDropdown,
|
|
updateIndicators: updateThemeIndicators,
|
|
});
|
|
|
|
// Also keep the window export temporarily (remove in Phase 2)
|
|
(window as any).initializeThemeDropdown = initializeThemeDropdown;
|
|
(window as any).updateThemeIndicators = updateThemeIndicators;
|
|
```
|
|
|
|
**Complete File After Conversion:**
|
|
```typescript
|
|
// web/src/themeDropdown.ts - Rewritten version
|
|
|
|
import { toggleThemeDropdown } from "./header";
|
|
import { updateWoodPanelingIndicators } from "./woodPaneling";
|
|
import { Alpine } from "./alpine";
|
|
|
|
// Composed theme dropdown initialization
|
|
export function initializeThemeDropdown(): void {
|
|
// Call original function from header.ts
|
|
toggleThemeDropdown();
|
|
// Then update theme indicators
|
|
updateThemeIndicators();
|
|
// Update wood paneling indicators
|
|
updateWoodPanelingIndicators();
|
|
}
|
|
|
|
// Update theme indicator elements (existing function, keep as-is)
|
|
export function updateThemeIndicators(): void {
|
|
const themeSelect = document.getElementById("theme-select") as HTMLSelectElement;
|
|
const themeIndicator = document.getElementById("current-theme");
|
|
|
|
if (themeSelect && themeIndicator) {
|
|
const theme = themeSelect.value as ThemeType;
|
|
themeIndicator.textContent = theme.charAt(0).toUpperCase() + theme.slice(1);
|
|
}
|
|
|
|
// Update wood paneling button
|
|
const woodPanelingButton = document.getElementById("wood-paneling-button");
|
|
if (woodPanelingButton) {
|
|
const woodPaneling = localStorage.getItem("woodPaneling") || "none";
|
|
woodPanelingButton.textContent = woodPaneling === "none" ? "🪵" : "✓";
|
|
}
|
|
}
|
|
|
|
// Register with Alpine for template access
|
|
Alpine.global("themeDropdown", {
|
|
initialize: initializeThemeDropdown,
|
|
updateIndicators: updateThemeIndicators,
|
|
});
|
|
|
|
// Temporary window exports (remove in Phase 2)
|
|
(window as any).initializeThemeDropdown = initializeThemeDropdown;
|
|
(window as any).updateThemeIndicators = updateThemeIndicators;
|
|
```
|
|
|
|
**Changes Summary:**
|
|
- ✅ Added 3 ES imports (toggleThemeDropdown, updateWoodPanelingIndicators, Alpine)
|
|
- ✅ Deleted 8 lines of window read/override code
|
|
- ✅ Created 1 new composed function (initializeThemeDropdown)
|
|
- ✅ Added Alpine.global() registration (7 lines)
|
|
- ✅ Kept window exports temporarily (2 lines, remove in Phase 2)
|
|
|
|
---
|
|
|
|
#### 1.4.2: web/src/docs.ts - Window Export Conversion Only
|
|
|
|
**⚠️ IMPORTANT:** Search functionality changes (Lunr removal, backend API) are handled by a **separate DOCS_SEARCH_IMPLEMENTATION.md task**. This section ONLY covers ESBuild migration changes (window export → Alpine.global()).
|
|
|
|
---
|
|
|
|
**Current Issue (Line 91):**
|
|
```typescript
|
|
(window as any).toggleSidebar = toggleSidebar;
|
|
```
|
|
|
|
This is the only window global that needs to be converted for the ESBuild migration.
|
|
|
|
**Note:** The search functionality (`performDocsSearch`, `initializeDocsSearch`, Lunr imports, etc.) will be addressed separately by the backend API search implementation (see DOCS_SEARCH_IMPLEMENTATION.md).
|
|
|
|
---
|
|
|
|
**Step-by-Step Conversion:**
|
|
|
|
**1. Add Alpine Import (after line 1):**
|
|
```typescript
|
|
// BEFORE:
|
|
import * as lunr from "lunr";
|
|
|
|
// AFTER:
|
|
import * as lunr from "lunr";
|
|
import { Alpine } from "./alpine";
|
|
```
|
|
|
|
**2. Remove Window Export and Add Alpine Registration (replace line 91):**
|
|
```typescript
|
|
// DELETE line 91:
|
|
// (window as any).toggleSidebar = toggleSidebar;
|
|
|
|
// REPLACE with:
|
|
// Register with Alpine for template access
|
|
Alpine.global("docs", {
|
|
toggleSidebar,
|
|
initializeSearch: initializeDocsSearch,
|
|
});
|
|
```
|
|
|
|
**3. Add ES Module Export (at end of file):**
|
|
```typescript
|
|
export { toggleSidebar, initializeDocsSearch };
|
|
```
|
|
|
|
---
|
|
|
|
**Complete Converted File (ESBuild changes only):**
|
|
```typescript
|
|
// web/src/docs.ts - ESBuild migration changes
|
|
|
|
import * as lunr from "lunr";
|
|
import { Alpine } from "./alpine";
|
|
|
|
function toggleSidebar(): void {
|
|
const sidebar = document.getElementById("docs-sidebar");
|
|
const overlay = document.getElementById("docs-overlay");
|
|
|
|
if (sidebar && overlay) {
|
|
sidebar.classList.toggle("translate-x-0");
|
|
sidebar.classList.toggle("-translate-x-full");
|
|
overlay.classList.toggle("hidden");
|
|
}
|
|
}
|
|
|
|
function initializeDocsSearch(): void {
|
|
const searchInput = document.getElementById(
|
|
"docs-search",
|
|
) as HTMLInputElement;
|
|
const searchResults = document.getElementById("docs-search-results");
|
|
|
|
if (!searchInput || !searchResults) return;
|
|
|
|
let docsSearchTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
searchInput.addEventListener("input", () => {
|
|
const query = searchInput.value.trim();
|
|
|
|
if (docsSearchTimeout) {
|
|
clearTimeout(docsSearchTimeout);
|
|
}
|
|
|
|
if (query.length < 2) {
|
|
searchResults.innerHTML = "";
|
|
searchResults.classList.add("hidden");
|
|
return;
|
|
}
|
|
|
|
docsSearchTimeout = setTimeout(() => {
|
|
performDocsSearch(query);
|
|
}, 300);
|
|
});
|
|
}
|
|
|
|
function performDocsSearch(query: string): void {
|
|
const searchResults = document.getElementById("docs-search-results");
|
|
if (!searchResults) return;
|
|
|
|
try {
|
|
const idx = lunr.Index.load(lunrIndexData);
|
|
if (!idx) {
|
|
searchResults.innerHTML =
|
|
'<p class="p-2 text-sm" style="color: var(--text-secondary)">Search index not loaded</p>';
|
|
searchResults.classList.remove("hidden");
|
|
return;
|
|
}
|
|
|
|
const results = idx.search(query);
|
|
|
|
if (results.length === 0) {
|
|
searchResults.innerHTML =
|
|
'<p class="p-2 text-sm" style="color: var(--text-secondary)">No results found</p>';
|
|
} else {
|
|
searchResults.innerHTML = results
|
|
.slice(0, 10)
|
|
.map((result: { ref: string }) => {
|
|
const doc = docsData[result.ref];
|
|
if (!doc) return "";
|
|
|
|
return `
|
|
<a href="${result.ref}" class="block p-2 hover:bg-opacity-50 transition-colors" style="background-color: var(--bg-secondary)">
|
|
<p class="font-medium text-sm" style="color: var(--text-primary)">${doc.title || result.ref}</p>
|
|
${doc.section ? `<p class="text-xs" style="color: var(--text-secondary)">${doc.section}</p>` : ""}
|
|
</a>
|
|
`;
|
|
})
|
|
.join("");
|
|
}
|
|
|
|
searchResults.classList.remove("hidden");
|
|
} catch (error) {
|
|
console.error("Search error:", error);
|
|
searchResults.innerHTML =
|
|
'<p class="p-2 text-sm" style="color: var(--text-secondary)">Search error</p>';
|
|
searchResults.classList.remove("hidden");
|
|
}
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
initializeDocsSearch();
|
|
});
|
|
|
|
// Register with Alpine for template access
|
|
Alpine.global("docs", {
|
|
toggleSidebar,
|
|
initializeSearch: initializeDocsSearch,
|
|
});
|
|
|
|
// ES module exports
|
|
export { toggleSidebar, initializeDocsSearch };
|
|
```
|
|
|
|
---
|
|
|
|
**ESBuild Migration Changes Only:**
|
|
- ✅ Added 1 import (Alpine from "./alpine")
|
|
- ✅ Removed 1 line (window export)
|
|
- ✅ Added 7 lines (Alpine.global() registration)
|
|
- ✅ Added 1 line (ES module export)
|
|
|
|
**Search Functionality:** Unchanged (handled by separate DOCS_SEARCH_IMPLEMENTATION.md task)
|
|
|
|
**Note:** The `lunrIndexData` and `docsData` undefined variables will be fixed when the backend API search is implemented (see DOCS_SEARCH_IMPLEMENTATION.md).
|
|
|
|
---
|
|
|
|
#### 1.4.3: No Other Files Need Function Wrapping Changes
|
|
|
|
**Confirmed Analysis:**
|
|
- ✅ `header.ts` - Only exports functions, no wrapping
|
|
- ✅ `woodPaneling.ts` - Only exports functions, no wrapping
|
|
- ✅ All other consumer files - Only import/use functions, no wrapping
|
|
|
|
**Only 2 files affected:** themeDropdown.ts and docs.ts
|
|
|
|
---
|
|
|
|
#### 1.4.4: Verification Steps
|
|
|
|
After completing both file conversions:
|
|
|
|
```bash
|
|
# Build TypeScript
|
|
npm run build:ts
|
|
|
|
# Run application
|
|
go run .
|
|
|
|
# Test in browser:
|
|
# 1. Navigate to dashboard
|
|
# 2. Click theme dropdown - should work and update indicators
|
|
# 3. Navigate to /docs
|
|
# 4. Search in docs - should work without console errors
|
|
```
|
|
|
|
**Expected Results:**
|
|
- ✅ Theme dropdown opens and updates indicators
|
|
- ✅ Wood paneling indicators update correctly
|
|
- ✅ Docs search works (lunr loaded via ES import)
|
|
- ✅ No console errors about missing window.lunr
|
|
- ✅ No console errors about missing window.lunrIndex
|
|
- ✅ No console errors about missing window.docsData
|
|
|
|
**Common Issues:**
|
|
- If theme dropdown doesn't open: Check if toggleThemeDropdown import is correct
|
|
- If indicators don't update: Check if updateWoodPanelingIndicators is called
|
|
- If docs search fails: Check if lunr is imported correctly (not via window)
|
|
- If data missing: Check if fetch() API is working (not using window globals)
|
|
|
|
### Step 1.5: Remove Window Exports from Consumer Files (If Present)
|
|
|
|
After converting all reads to imports, check if consumer files have window exports to remove.
|
|
|
|
**Note:** Not all files have window exports. Some files (like dashboard.ts) use event delegation and don't export to window.
|
|
|
|
**Files WITH window exports to remove:**
|
|
|
|
**web/src/library.ts** (lines 690-704)
|
|
```typescript
|
|
// DELETE these window exports:
|
|
(window as any).deleteLibrary = deleteLibrary;
|
|
(window as any).showLibraryFolders = showLibraryFolders;
|
|
(window as any).addLibraryFolder = addLibraryFolder;
|
|
(window as any).removeLibraryFolder = removeLibraryFolder;
|
|
(window as any).setLibraryVisibility = setLibraryVisibility;
|
|
(window as any).loadUserVisibility = loadUserVisibility;
|
|
(window as any).editLibrary = editLibrary;
|
|
(window as any).handleCreateLibrarySubmit = handleCreateLibrarySubmit;
|
|
(window as any).showFolderBrowser = showFolderBrowser;
|
|
(window as any).navigateFolderBrowser = navigateFolderBrowser;
|
|
(window as any).selectBrowseFolder = selectBrowseFolder;
|
|
(window as any).hideFolderBrowser = hideFolderBrowser;
|
|
(window as any).showDeleteModal = showDeleteModal;
|
|
(window as any).hideDeleteModal = hideDeleteModal;
|
|
(window as any).confirmDeleteLibrary = confirmDeleteLibrary;
|
|
```
|
|
|
|
**web/src/collections.ts** (multiple locations: lines 204-208, 243, 315-317, 472-476, 912-923)
|
|
```typescript
|
|
// DELETE all window exports (26 functions total)
|
|
// Examples:
|
|
(window as any).loadCollections = loadCollections;
|
|
(window as any).loadCollectionRules = loadCollectionRules;
|
|
// ... etc (all 26 exports)
|
|
```
|
|
|
|
**web/src/bookshelf.ts** (lines 127-130)
|
|
```typescript
|
|
// DELETE these window exports:
|
|
(window as any).selectLibrary = selectLibrary;
|
|
(window as any).loadBookshelf = loadBookshelf;
|
|
(window as any).selectBook = selectBook;
|
|
(window as any).changePage = changePage;
|
|
```
|
|
|
|
**web/src/api-explorer.ts** (lines 186-189)
|
|
```typescript
|
|
// DELETE these window exports:
|
|
(window as any).sendApiRequest = sendApiRequest;
|
|
(window as any).loadFromHistory = loadFromHistory;
|
|
(window as any).copyCurl = copyCurl;
|
|
(window as any).formatJson = formatJson;
|
|
```
|
|
|
|
**Files WITHOUT window exports (already clean):**
|
|
- ✅ **dashboard.ts** - Uses event delegation, no window exports
|
|
- ✅ **admin.ts** - Check if it has exports
|
|
- ✅ **queue.ts** - Check if it has exports
|
|
- ✅ **conflicts.ts** - Check if it has exports
|
|
- ✅ **analytics.ts** - Check if it has exports
|
|
- ✅ **device-management.ts** - Check if it has exports
|
|
- ✅ **linking.ts** - Check if it has exports
|
|
- ✅ **custom-section-builder.ts** - Check if it has exports
|
|
|
|
**Verification:**
|
|
For each file, search for `(window as any).` at the end of the file. If present, remove those lines.
|
|
|
|
**Only keep Alpine.global()** for functions that templates call directly.
|
|
|
|
### Step 1.6: Verify Build and Test
|
|
|
|
```bash
|
|
npm run build:ts
|
|
go run .
|
|
```
|
|
|
|
**Verification**:
|
|
- Build succeeds
|
|
- All pages still work (dashboard, library, collections, etc.)
|
|
- Console shows no errors
|
|
- All onclick handlers still work
|
|
|
|
**Why this works**: Internal dependencies now use imports, but window exports still exist for templates. App is fully functional.
|
|
|
|
---
|
|
|
|
## Phase 2: Dual Exports - ES Modules + Alpine Bridge
|
|
|
|
**Goal**: Establish Alpine.js as the bridge to templates while keeping ES exports for TypeScript.
|
|
|
|
**Files**: All files that export functions used by templates
|
|
|
|
**Duration**: 2-3 hours
|
|
|
|
### Step 2.1: Ensure Alpine Initialization File
|
|
|
|
**web/src/alpine.ts** (Already exists, verify it's correct)
|
|
```typescript
|
|
import Alpine from "alpinejs";
|
|
|
|
// Extend Window interface
|
|
declare global {
|
|
interface Window {
|
|
Alpine: typeof Alpine;
|
|
}
|
|
}
|
|
|
|
// Initialize Alpine
|
|
window.Alpine = Alpine;
|
|
Alpine.start();
|
|
|
|
// Re-export for other modules
|
|
export { Alpine };
|
|
```
|
|
|
|
### Step 2.2: Register Template Functions with Alpine
|
|
|
|
For each file that templates call, register functions with Alpine:
|
|
|
|
**web/src/api.ts** (Already done ✅)
|
|
```typescript
|
|
Alpine.global("api", {
|
|
get: apiGet,
|
|
post: apiPost,
|
|
// ... etc
|
|
});
|
|
```
|
|
|
|
**web/src/toast.ts** (Already done ✅)
|
|
```typescript
|
|
Alpine.global("showToast", {
|
|
error: (message: string, duration?: number) => showToast(message, "error", duration),
|
|
success: (message: string, duration?: number) => showToast(message, "success", duration),
|
|
info: (message: string, duration?: number) => showToast(message, "info", duration),
|
|
});
|
|
```
|
|
|
|
**web/src/header.ts**
|
|
```typescript
|
|
Alpine.global("header", {
|
|
logout,
|
|
toggleThemeDropdown,
|
|
toggleUserMenu,
|
|
changeThemeTo: (theme: string) => {
|
|
changeThemeTo(theme);
|
|
updateThemeIndicators(); // Call themeDropdown function
|
|
},
|
|
});
|
|
```
|
|
|
|
**web/src/library.ts**
|
|
```typescript
|
|
Alpine.global("library", {
|
|
deleteLibrary,
|
|
showLibraryFolders,
|
|
addLibraryFolder,
|
|
removeLibraryFolder,
|
|
});
|
|
```
|
|
|
|
**web/src/collections.ts**
|
|
```typescript
|
|
Alpine.global("collections", {
|
|
loadCollections,
|
|
createRule,
|
|
deleteRule,
|
|
testRule,
|
|
navigateToCollection,
|
|
selectColor,
|
|
closeCollectionModal,
|
|
initColorSelection,
|
|
selectIcon,
|
|
filterIcons,
|
|
showAllIcons,
|
|
populateIconGrid,
|
|
initIconSelection,
|
|
initCollectionDetail,
|
|
showAddBooksModal,
|
|
hideAddBooksModal,
|
|
searchBooksForCollections,
|
|
toggleBookSelection,
|
|
addbooksToAdd,
|
|
removeBook,
|
|
toggleBookForRemoval,
|
|
updateSelectedCount,
|
|
removebooksToAdd,
|
|
filterCollectionBooks,
|
|
backToCollections,
|
|
});
|
|
```
|
|
|
|
**web/src/admin.ts**
|
|
```typescript
|
|
Alpine.global("admin", {
|
|
triggerLibraryScan,
|
|
triggerQuickScan,
|
|
loadSystemStats,
|
|
scanAllLibraries,
|
|
loadWatchStatus,
|
|
hideScanProgress,
|
|
stopScanStatusPolling,
|
|
});
|
|
```
|
|
|
|
**web/src/queue.ts**
|
|
```typescript
|
|
Alpine.global("queue", {
|
|
refreshQueue,
|
|
processPendingItems,
|
|
clearFailedItems,
|
|
clearAllItems,
|
|
retryQueueItem,
|
|
deleteQueueItem,
|
|
});
|
|
```
|
|
|
|
**web/src/conflicts.ts**
|
|
```typescript
|
|
Alpine.global("conflicts", {
|
|
refreshConflicts,
|
|
resolveConflict,
|
|
bulkResolve,
|
|
bulkDismiss,
|
|
dismissAllResolved,
|
|
showResolveModal,
|
|
hideResolveModal,
|
|
handleResolveSubmit,
|
|
});
|
|
```
|
|
|
|
**web/src/docs.ts**
|
|
```typescript
|
|
Alpine.global("docs", {
|
|
toggleSidebar,
|
|
initializeSearch: initializeDocsSearch,
|
|
});
|
|
```
|
|
|
|
**web/src/search.ts**
|
|
```typescript
|
|
Alpine.global("search", {
|
|
selectLibraryAndBook,
|
|
});
|
|
```
|
|
|
|
**web/src/device-management.ts**
|
|
```typescript
|
|
Alpine.global("devices", {
|
|
copyToClipboard,
|
|
regenerateDeviceToken,
|
|
});
|
|
```
|
|
|
|
**web/src/password_validation.ts**
|
|
```typescript
|
|
Alpine.global("validation", {
|
|
initPasswordValidation,
|
|
});
|
|
```
|
|
|
|
### Step 2.3: Remove Old Window Exports
|
|
|
|
After Alpine registration, REMOVE the old window export lines:
|
|
|
|
**Before**:
|
|
```typescript
|
|
(window as any).api = { ... };
|
|
Alpine.global("api", { ... });
|
|
```
|
|
|
|
**After**:
|
|
```typescript
|
|
Alpine.global("api", { ... });
|
|
```
|
|
|
|
Systematically search and remove these patterns from all files.
|
|
|
|
### Step 2.4: Verify Build
|
|
|
|
```bash
|
|
npm run build:ts
|
|
go run .
|
|
```
|
|
|
|
**Verification**:
|
|
- Build succeeds
|
|
- All pages still work
|
|
- Alpine is loaded (check browser DevTools: `window.Alpine` should be defined)
|
|
- Console shows no errors
|
|
|
|
**Why this works**: Functions now registered with Alpine instead of window, but templates still use onclick="func()" so they still work.
|
|
|
|
---
|
|
|
|
## Phase 3: Template Migration (Incremental)
|
|
|
|
**Goal**: Migrate templates one-by-one from onclick handlers to Alpine directives.
|
|
|
|
**Strategy**: One template at a time, test each, can ship after each migration.
|
|
|
|
**Duration**: 1-2 hours per template (27 templates = 27-54 hours total)
|
|
|
|
### Template Migration Pattern
|
|
|
|
For each template file:
|
|
|
|
#### Step A: Remove Individual Script Tags
|
|
|
|
**Before**:
|
|
```templ
|
|
<script src="/static/htmx.min.js"></script>
|
|
<script src="/static/toast.js"></script>
|
|
<script src="/static/api.js"></script>
|
|
<script src="/static/events.js"></script>
|
|
<script src="/static/dom.js"></script>
|
|
<script src="/static/admin.js"></script>
|
|
```
|
|
|
|
**After**:
|
|
```templ
|
|
<script src="/static/htmx.min.js"></script>
|
|
<script src="/static/main.js"></script>
|
|
```
|
|
|
|
**Note**: Keep htmx.min.js separate (loaded before main.js)
|
|
|
|
#### Step B: Convert onclick to @click
|
|
|
|
**Before**:
|
|
```templ
|
|
<button onclick="toggleThemeDropdown()">Theme</button>
|
|
<div id="theme-dropdown" class="hidden">
|
|
<button onclick="changeThemeTo('tokyo-night')">Tokyo Night</button>
|
|
</div>
|
|
```
|
|
|
|
**After**:
|
|
```templ
|
|
<button @click="header.toggleThemeDropdown()">Theme</button>
|
|
<div x-show="themeDropdownOpen" @click.outside="themeDropdownOpen = false" x-transition>
|
|
<button @click="header.changeThemeTo('tokyo-night')">Tokyo Night</button>
|
|
</div>
|
|
```
|
|
|
|
#### Step C: Add Alpine State for UI Components
|
|
|
|
For modals, dropdowns, and any UI with show/hide state:
|
|
|
|
**Before**:
|
|
```templ
|
|
<button onclick="showAddBooksModal()">Add Books</button>
|
|
<div id="add-books-modal" class="hidden">
|
|
...modal content...
|
|
<button onclick="hideAddBooksModal()">Cancel</button>
|
|
</div>
|
|
```
|
|
|
|
**After**:
|
|
```templ
|
|
<div x-data="{ addBooksModalOpen: false }">
|
|
<button @click="addBooksModalOpen = true">Add Books</button>
|
|
<div x-show="addBooksModalOpen"
|
|
x-transition
|
|
@click.self="addBooksModalOpen = false"
|
|
class="fixed inset-0 ...">
|
|
...modal content...
|
|
<button @click="addBooksModalOpen = false">Cancel</button>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
|
|
**Important**: Alpine uses namespace objects (`api.post`, `showToast.success`),
|
|
**while TypeScript code uses individual functions** (`apiPost`, `showToast`).
|
|
**This is correct** - see Phase 2 for how Alpine creates these namespace objects.
|
|
|
|
#### Step D: Update Namespace Calls
|
|
|
|
**Before**:
|
|
```templ
|
|
<button onclick="showToast.success('Saved!')">Save</button>
|
|
<button onclick="apiPost('/api/save', data)">Save</button>
|
|
```
|
|
|
|
**After**:
|
|
```templ
|
|
<button @click="showToast.success('Saved!')">Save</button>
|
|
<button @click="api.post('/api/save', data)">Save</button>
|
|
```
|
|
|
|
### Template Order (Low Risk to High Risk)
|
|
|
|
#### Batch 1: Simple Pages (No complex state)
|
|
1. **login.templ** - Only has showToast, api
|
|
2. **register.templ** - Only has showToast, api, validation
|
|
3. **profile.templ** - Only has showToast, api
|
|
4. **settings.templ** - Only has showToast, api
|
|
|
|
#### Batch 2: Pages with Simple State
|
|
5. **stats.templ** - Only has showToast, api, events
|
|
6. **sync.templ** - Only has showToast, api
|
|
7. **progress.templ** - Only has showToast, api, events
|
|
8. **custom_section.templ** - Has modal, but simple
|
|
|
|
#### Batch 3: Pages with Moderate State
|
|
9. **collection_rules.templ** - Has showToast, api, events, some state
|
|
10. **queue.templ** - Has queue namespace, multiple modals
|
|
11. **conflicts.templ** - Has conflicts namespace, modal
|
|
12. **admin.templ** - Has admin namespace, modal
|
|
13. **admin_library.templ** - Has library namespace, multiple modals
|
|
14. **admin_users.templ** - Has header functions, simple
|
|
|
|
#### Batch 4: Complex Pages
|
|
15. **dashboard.templ** - Has theme dropdown, wood paneling, collections
|
|
16. **bookshelf.templ** - Has search, book viewing, shelf mappings
|
|
17. **devices.templ** - Has devices namespace, multiple modals
|
|
18. **analytics.templ** - Keep Chart.js on CDN, has analytics namespace
|
|
19. **api_explorer.templ** - Has explorer namespace
|
|
|
|
#### Batch 5: Most Complex (Shared Components)
|
|
20. **collections.templ** - Has collections namespace, multiple modals, color/icon pickers
|
|
21. **docs.templ** - Has sidebar toggle, search, inline JS to migrate
|
|
22. **header.templ** - Used in 17 templates! Theme dropdown, user menu
|
|
|
|
#### Batch 6: Modal/Component Templates
|
|
23. **collection_modal.templ** - Shared component
|
|
24. **restore_system_collection_modal.templ** - Shared component
|
|
25. **profile_modal.templ** - Shared component
|
|
26. **profile_form.templ** - Shared component
|
|
27. **toast.templ** - Shared component (if using template for toast)
|
|
|
|
### Special Cases
|
|
|
|
#### docs.templ - Migrate Inline JavaScript
|
|
|
|
**Current** (lines 124-515): Large inline script block
|
|
|
|
**New Approach**: Move to docs.ts
|
|
|
|
```typescript
|
|
// web/src/docs.ts
|
|
export function initializeDocsPage() {
|
|
// Move toggleSection, toggleSidebar functions here
|
|
// Move search initialization here
|
|
// Keep using fetch for data loading (SSR-compatible)
|
|
}
|
|
|
|
Alpine.global("docs", {
|
|
initializePage: initializeDocsPage,
|
|
toggleSection,
|
|
toggleSidebar,
|
|
});
|
|
```
|
|
|
|
```templ
|
|
<!-- templates/docs.templ -->
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
window.Alpine.docs.initializePage();
|
|
});
|
|
</script>
|
|
```
|
|
|
|
#### header.templ - Most Critical (Used in 17 Places)
|
|
|
|
This template is included in 17 other templates. Test thoroughly:
|
|
|
|
```templ
|
|
<!-- Before -->
|
|
<button onclick="toggleThemeDropdown()">Theme</button>
|
|
<div id="theme-dropdown" class="hidden">
|
|
<button onclick="changeThemeTo('tokyo-night')">Tokyo Night</button>
|
|
...11 more themes...
|
|
<button onclick="changeWoodPaneling('none')">None</button>
|
|
<button onclick="changeWoodPaneling('wood-light')">Wood Light</button>
|
|
</div>
|
|
|
|
<button onclick="toggleUserMenu()">
|
|
<div id="user-menu" class="hidden">
|
|
<button onclick="logout()">Logout</button>
|
|
</div>
|
|
```
|
|
|
|
```templ
|
|
<!-- After -->
|
|
<div x-data="{ themeDropdownOpen: false, userMenuOpen: false }">
|
|
<button @click="themeDropdownOpen = !themeDropdownOpen">Theme</button>
|
|
<div x-show="themeDropdownOpen"
|
|
@click.outside="themeDropdownOpen = false"
|
|
x-transition:enter="transition ease-out duration-200"
|
|
x-transition:enter-start="opacity-0 scale-95"
|
|
x-transition:enter-end="opacity-100 scale-100"
|
|
class="absolute right-0 mt-2 w-64 rounded-lg shadow-lg z-50">
|
|
<button @click="header.changeThemeTo('tokyo-night'); themeDropdownOpen = false">Tokyo Night</button>
|
|
...11 more themes...
|
|
<button @click="woodPaneling.change('none'); themeDropdownOpen = false">None</button>
|
|
<button @click="woodPaneling.change('wood-light'); themeDropdownOpen = false">Wood Light</button>
|
|
</div>
|
|
|
|
<button @click="userMenuOpen = !userMenuOpen">
|
|
<div x-show="userMenuOpen"
|
|
@click.outside="userMenuOpen = false"
|
|
x-transition>
|
|
<button @click="header.logout(); userMenuOpen = false">Logout</button>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
### Testing Strategy for Each Template
|
|
|
|
After migrating each template:
|
|
|
|
1. **Build**: `npm run build:ts && templ generate`
|
|
2. **Run**: `go run .`
|
|
3. **Test**: Visit the page, test all interactions:
|
|
- All buttons work
|
|
- Modals open/close
|
|
- Dropdowns work
|
|
- Forms submit (via HTMX)
|
|
- Toast notifications appear
|
|
- No console errors
|
|
4. **Verify**: Check Alpine DevTools (if installed) for reactive state
|
|
|
|
### Rollback Strategy
|
|
|
|
If a migrated template has issues:
|
|
|
|
```bash
|
|
# Revert the template file
|
|
git checkout templates/PROBLEM_TEMPLATE.templ
|
|
|
|
# Rebuild
|
|
templ generate
|
|
go run .
|
|
```
|
|
|
|
The template is now using old onclick handlers, but the bundle still has the functions registered. Everything works.
|
|
|
|
---
|
|
|
|
## Phase 4: Data Injection (Server → Client)
|
|
|
|
**Goal**: Ensure server-to-client data flow works correctly without window globals.
|
|
|
|
**Files**: docs.templ, any future templates that need data injection
|
|
|
|
**Duration**: 1-2 hours
|
|
|
|
### Current Situation
|
|
|
|
**docs.templ** currently loads data via fetch (correct approach):
|
|
```javascript
|
|
// In template inline JS (lines 228-285)
|
|
fetch('/docs/search-index.json')
|
|
.then(r => r.json())
|
|
.then(data => { searchDocs = data; })
|
|
```
|
|
|
|
**Go handler provides**:
|
|
```go
|
|
// internal/docs/http_handler.go line 417
|
|
func (h *HTTPHandler) ServeSearchIndex(c *echo.Context) error {
|
|
index, err := h.docs.GenerateSearchIndex();
|
|
return c.JSON(http.StatusOK, index)
|
|
}
|
|
```
|
|
|
|
### Migration Strategy
|
|
|
|
Keep this pattern! It's already correct:
|
|
|
|
1. **Server provides JSON endpoints** for data
|
|
2. **Client fetches data** via APIs
|
|
3. **No window globals** needed
|
|
|
|
### Future Templates (Dashboard Pattern)
|
|
|
|
For new pages that follow the dashboard pattern:
|
|
|
|
1. **Server renders HTML** with initial data (SSR)
|
|
2. **Client fetches updates** via API when needed
|
|
3. **No data injection into window**
|
|
|
|
Example:
|
|
```go
|
|
// Go handler
|
|
func (h *Handler) ShowDashboard(c *echo.Context) error {
|
|
// Fetch data from database
|
|
collections := h.queries.GetCollections(c.request().Context())
|
|
|
|
// Render template with data
|
|
return templates.Dashboard(collections).Render(c)
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
// Client-side (if needed)
|
|
import { apiGet, apiPost, apiPut, apiDelete, apiPatch, handleResponse, handleVoidResponse, handleError } from "./api";
|
|
|
|
async function refreshCollections() {
|
|
const response = await apiGet("/collections");
|
|
const data = await handleResponse(response);
|
|
// Update DOM
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 5: Cleanup
|
|
|
|
**Goal**: Remove all legacy code and old .js files.
|
|
|
|
**Duration**: 1-2 hours
|
|
|
|
### Step 5.1: Remove Individual .js Files
|
|
|
|
After all 27 templates are migrated and verified working:
|
|
|
|
```bash
|
|
cd /home/nymusicman/Code/bookhoard/web/static
|
|
|
|
# Remove old individual JS files
|
|
rm -f api.js toast.js events.js dom.js storage.js
|
|
rm -f theme.js header.js themeDropdown.js woodPaneling.js
|
|
rm -f search.js docs.js collections.js conflicts.js
|
|
rm -f dashboard.js admin.js admin_library.js admin_users.js
|
|
rm -f analytics.js bookshelf.js devices.js
|
|
rm -f index.js login.js profile.js
|
|
rm -f queue.js custom_section.js progress.js
|
|
rm -f register.js settings.js stats.js sync.js
|
|
rm -f testing_templ.js obsidian.js whatsapp.js midnight.js sunset.js
|
|
rm -f password_validation.js api_explorer.js
|
|
|
|
# Keep only:
|
|
# - main.js (bundled)
|
|
# - main.js.map (sourcemap)
|
|
# - htmx.min.js (separate load)
|
|
# - highlight.min.js (for docs)
|
|
# - style.css (Tailwind)
|
|
```
|
|
|
|
### Step 5.2: Remove All Window Exports
|
|
|
|
From all TypeScript files, remove any remaining `(window as any)` exports:
|
|
|
|
```bash
|
|
# Search for remaining window exports
|
|
cd /home/nymusicman/Code/bookhoard/web/src
|
|
grep -rn "window as any" *.ts
|
|
```
|
|
|
|
Should only find:
|
|
- Type assertions being removed
|
|
- Comments referencing old pattern
|
|
|
|
Remove all:
|
|
```typescript
|
|
// DELETE these lines:
|
|
(window as any).functionName = functionName;
|
|
```
|
|
|
|
### Step 5.3: Update Build Scripts
|
|
|
|
Verify package.json scripts are correct:
|
|
|
|
```json
|
|
{
|
|
"scripts": {
|
|
"build:css": "tailwindcss -i ./web/static/input.css -o ./web/static/style.css --watch",
|
|
"build:css:prod": "tailwindcss -i ./web/static/input.css -o ./web/static/style.css --minify",
|
|
"build:ts": "esbuild web/src/main.ts --bundle --outfile=web/static/main.js --sourcemap --target=es2020 --minify",
|
|
"build:ts:dev": "esbuild web/src/main.ts --bundle --outfile=web/static/main.js --sourcemap --target=es2020",
|
|
"build:ts:watch": "esbuild web/src/main.ts --bundle --outfile=web/static/main.js --sourcemap --target=es2020 --watch",
|
|
"build": "npm run build:ts && npm run build:css:prod",
|
|
"dev": "npm run build:ts:dev && templ generate && go run ."
|
|
}
|
|
}
|
|
```
|
|
|
|
### Step 5.4: Final Verification
|
|
|
|
```bash
|
|
# Complete build
|
|
npm run build
|
|
templ generate
|
|
go build
|
|
|
|
# Check bundle size
|
|
ls -lh web/static/main.js
|
|
# Expected: ~120-150KB
|
|
|
|
# Run tests (if any)
|
|
go test ./...
|
|
|
|
# Manual testing
|
|
go run .
|
|
# Test all pages, verify functionality
|
|
```
|
|
|
|
### Step 5.5: Update Documentation
|
|
|
|
Update or remove any references to old build process in documentation.
|
|
|
|
---
|
|
|
|
## Success Criteria
|
|
|
|
### Phase 0 Completion
|
|
✅ All utility modules have ES exports
|
|
✅ Build succeeds
|
|
✅ No functionality broken
|
|
|
|
### Phase 1 Completion
|
|
✅ All 193+ internal window reads converted to imports
|
|
✅ All consumer files use ES module imports
|
|
✅ Function wrapping eliminated (themeDropdown.ts restructured)
|
|
✅ Build succeeds
|
|
✅ All pages still work
|
|
|
|
### Phase 2 Completion
|
|
✅ All template functions registered with Alpine
|
|
✅ Old window exports removed
|
|
✅ Build succeeds
|
|
✅ Alpine loaded and functional
|
|
✅ All pages still work (onclick still works)
|
|
|
|
### Phase 3 Completion
|
|
✅ All 27 templates migrated to @click
|
|
✅ All individual script tags replaced with single main.js
|
|
✅ Inline JS migrated to TypeScript where appropriate
|
|
✅ All onclick handlers converted to @click
|
|
✅ All UI state uses x-data/x-show
|
|
✅ All templates tested and working
|
|
|
|
### Phase 4 Completion
|
|
✅ Server data injection uses API endpoints (not window)
|
|
✅ Docs search still works
|
|
✅ Future pages follow SSR-first pattern
|
|
|
|
### Phase 5 Completion
|
|
✅ All old .js files removed
|
|
✅ No window globals remain
|
|
✅ Only main.js and main.js.map exist
|
|
✅ Bundle size ~120-150KB minified
|
|
✅ Clean codebase ready for launch
|
|
|
|
---
|
|
|
|
## Estimated Effort
|
|
|
|
| Phase | Duration | Risk | Can Ship After |
|
|
|-------|----------|------|----------------|
|
|
| Phase 0 | 1-2 hours | Low | ✅ Yes |
|
|
| Phase 1 | 4-6 hours | Medium | ✅ Yes |
|
|
| Phase 2 | 2-3 hours | Low | ✅ Yes |
|
|
| Phase 3 | 27-54 hours | Medium-High | ✅ Yes (per template) |
|
|
| Phase 4 | 1-2 hours | Low | ✅ Yes |
|
|
| Phase 5 | 1-2 hours | Low | ❌ No (final cleanup) |
|
|
| **Total** | **36-69 hours** | | |
|
|
|
|
**Recommended Schedule**:
|
|
- Week 1: Phases 0-2 (Foundation) - 7-11 hours
|
|
- Week 2-4: Phase 3 (Templates) - 5-10 templates per week
|
|
- Week 5: Phases 4-5 (Finalize) - 2-4 hours
|
|
|
|
---
|
|
|
|
## Rollback Procedures
|
|
|
|
### If Phase 0 or Phase 1 Fails
|
|
|
|
```bash
|
|
# Revert TypeScript changes
|
|
git checkout web/src/
|
|
|
|
# Rebuild
|
|
npm run build:ts
|
|
go run .
|
|
```
|
|
|
|
### If Phase 2 Fails
|
|
|
|
```bash
|
|
# Revert to Phase 1 state (window exports still present)
|
|
git checkout web/src/
|
|
|
|
# Rebuild
|
|
npm run build:ts
|
|
go run .
|
|
```
|
|
|
|
### If Phase 3 (Template Migration) Fails
|
|
|
|
```bash
|
|
# Revert specific problematic template
|
|
git checkout templates/PROBLEM_TEMPLATE.templ
|
|
|
|
# Regenerate templates
|
|
templ generate
|
|
|
|
# Rebuild
|
|
go run .
|
|
```
|
|
|
|
All other migrated templates continue working.
|
|
|
|
---
|
|
|
|
## Testing Checklist
|
|
|
|
### After Each Phase
|
|
|
|
- [ ] Build succeeds (`npm run build:ts`)
|
|
- [ ] Go build succeeds (`go build`)
|
|
- [ ] Application starts (`go run .`)
|
|
- [ ] Homepage loads
|
|
- [ ] Login works
|
|
- [ ] Dashboard loads
|
|
- [ ] No console errors
|
|
- [ ] Network tab shows no 404s for .js files
|
|
|
|
### After Phase 1 (Internal Dependencies)
|
|
|
|
- [ ] Dashboard works
|
|
- [ ] Library management works
|
|
- [ ] Collections work
|
|
- [ ] Admin functions work
|
|
- [ ] Queue works
|
|
- [ ] Conflicts work
|
|
- [ ] All toast notifications work
|
|
- [ ] All API calls work
|
|
|
|
### After Phase 3 (Each Template)
|
|
|
|
- [ ] Page loads
|
|
- [ ] All buttons work
|
|
- [ ] Modals open/close
|
|
- [ ] Dropdowns work
|
|
- [ ] Forms submit via HTMX
|
|
- [ ] Toast notifications appear
|
|
- [ ] No console errors
|
|
- [ ] Alpine DevTools shows reactive state (if installed)
|
|
|
|
---
|
|
|
|
## Files Modified Summary
|
|
|
|
### New Files Created
|
|
- None (alpine.ts already exists)
|
|
|
|
### Source Files Modified (21 files)
|
|
- web/src/main.ts (verify imports are correct)
|
|
- web/src/alpine.ts (verify initialization is correct)
|
|
- web/src/api.ts (add ES exports, Alpine already present)
|
|
- web/src/toast.ts (add ES exports, Alpine already present)
|
|
- web/src/storage.ts (already has exports ✅)
|
|
- web/src/events.ts (add ES exports and Alpine)
|
|
- web/src/dom.ts (already has exports ✅)
|
|
- web/src/theme.ts (add ES exports and Alpine)
|
|
- web/src/header.ts (add ES exports and Alpine, restructure)
|
|
- web/src/woodPaneling.ts (add ES exports and Alpine)
|
|
- web/src/themeDropdown.ts (RESTRUCTURE to eliminate wrapping)
|
|
- web/src/library.ts (convert 55 window reads to imports, add Alpine)
|
|
- web/src/collections.ts (convert 54 window reads to imports, add Alpine)
|
|
- web/src/dashboard.ts (convert 10 window reads to imports, add Alpine)
|
|
- web/src/admin.ts (convert 14 window reads to imports, add Alpine)
|
|
- web/src/queue.ts (convert 8 window reads to imports, add Alpine)
|
|
- web/src/conflicts.ts (convert 6 window reads to imports, add Alpine)
|
|
- web/src/linking.ts (convert window reads to imports, add Alpine)
|
|
- web/src/custom-section-builder.ts (convert window reads to imports, add Alpine)
|
|
- web/src/analytics.ts (convert window reads to imports, add Alpine)
|
|
- web/src/bookshelf.ts (convert window reads to imports, add Alpine)
|
|
- web/src/api-explorer.ts (convert window reads to imports, add Alpine)
|
|
- web/src/device-management.ts (convert window reads to imports, add Alpine)
|
|
- web/src/search.ts (add Alpine registration)
|
|
- web/src/docs.ts (remove window globals, move inline JS to module, add Alpine)
|
|
- web/src/password_validation.ts (add Alpine registration)
|
|
|
|
### Template Files Modified (27 files)
|
|
All templates updated to:
|
|
- Remove individual script tags
|
|
- Use single `<script src="/static/main.js"></script>`
|
|
- Replace `onclick` with `@click`
|
|
- Add `x-data` for stateful components (modals, dropdowns)
|
|
- Keep htmx.min.js separate
|
|
|
|
### Generated Files
|
|
- web/static/main.js (bundled output with Alpine)
|
|
- web/static/main.js.map (sourcemap)
|
|
|
|
### Files Deleted (Phase 5)
|
|
All individual .js files in web/static/ (except main.js, main.js.map, htmx.min.js, highlight.min.js, style.css)
|
|
|
|
---
|
|
|
|
## External Dependencies (Not Bundled)
|
|
|
|
### htmx.org
|
|
- **Status**: Keep as separate script tag
|
|
- **Reason**: Core framework, needs to load before main.js
|
|
- **Location**: `<script src="/static/htmx.min.js"></script>`
|
|
|
|
### Chart.js
|
|
- **Status**: Keep on CDN
|
|
- **Reason**: 3.4MB minified, only used on analytics page
|
|
- **Location**: `<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>` in analytics.templ only
|
|
|
|
### highlight.js
|
|
- **Status**: Bundled via ESBuild
|
|
- **Reason**: Used in docs, small enough (~5KB gzipped)
|
|
- **Import**: `import hljs from "highlight.js";`
|
|
|
|
### lunr
|
|
- **Status**: Bundled via ESBuild
|
|
- **Reason**: Used in docs search, small enough (~10KB gzipped)
|
|
- **Import**: `import * as lunr from "lunr";`
|
|
|
|
---
|
|
|
|
## Architecture Decision Records
|
|
|
|
### ADR-001: ES Modules over Window Globals
|
|
|
|
**Decision**: Use ES module imports/exports for all TypeScript-to-TypeScript dependencies.
|
|
|
|
**Rationale**:
|
|
- Standard JavaScript module system
|
|
- Better type safety with TypeScript
|
|
- Clear dependency chains
|
|
- Tree-shaking support
|
|
- No global namespace pollution
|
|
|
|
**Consequences**:
|
|
- Positive: Cleaner code, better IDE support, easier refactoring
|
|
- Positive: Standard pattern, easier for new developers
|
|
- Neutral: Requires build step (already using ESBuild)
|
|
|
|
### ADR-002: Alpine.js for Template Interactivity Only
|
|
|
|
**Decision**: Use Alpine.js ONLY as a bridge between templates and TypeScript, not for internal TypeScript dependencies.
|
|
|
|
**Rationale**:
|
|
- Alpine is designed for template directives (@click, x-show)
|
|
- Clean separation: ES modules for code, Alpine for templates
|
|
- Avoids over-engineering simple function calls
|
|
- Keeps bundle size smaller
|
|
|
|
**Consequences**:
|
|
- Positive: Clean template syntax
|
|
- Positive: Progressive enhancement works
|
|
- Positive: Easy to understand data flow
|
|
- Neutral: Need to learn Alpine basics (simple)
|
|
|
|
### ADR-003: SSR-First with Progressive Enhancement
|
|
|
|
**Decision**: Server renders complete HTML with data, client-side JavaScript only for interactivity.
|
|
|
|
**Rationale**:
|
|
- Faster initial page load
|
|
- Better SEO (if needed)
|
|
- Works without JavaScript (degrades gracefully)
|
|
- Simpler state management
|
|
- Aligns with HTMX philosophy
|
|
|
|
**Consequences**:
|
|
- Positive: Better performance
|
|
- Positive: More resilient
|
|
- Positive: Easier to debug
|
|
- Neutral: Slightly more server work (acceptable)
|
|
|
|
### ADR-004: Function Wrapping Elimination
|
|
|
|
**Decision**: Eliminate function wrapping (themeDropdown.ts wraps header.ts functions) in favor of proper module composition.
|
|
|
|
**Rationale**:
|
|
- Clearer code flow
|
|
- Better testability
|
|
- Easier to understand
|
|
- Standard pattern
|
|
- App not yet deployed, can refactor
|
|
|
|
**Consequences**:
|
|
- Positive: Cleaner architecture
|
|
- Positive: Easier to maintain
|
|
- Negative: More work upfront (acceptable)
|
|
- Negative: Need to restructure (acceptable)
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Build Errors
|
|
|
|
**Error**: "Cannot find module './xxx'"
|
|
|
|
**Solution**:
|
|
- Check import path is correct (relative, case-sensitive)
|
|
- Check file has `export {}` statements
|
|
- Run `npm run build:ts` with clean build
|
|
|
|
**Error**: "Alpine is not defined"
|
|
|
|
**Solution**:
|
|
- Check alpine.ts is imported in main.ts: `import "./alpine";`
|
|
- Check Alpine.start() is called
|
|
- Check window.Alpine is set
|
|
|
|
**Error**: "Cannot read property 'xxx' of undefined"
|
|
|
|
**Solution**:
|
|
- Check Alpine.global() is called after Alpine.start()
|
|
- Check namespace is correct (e.g., `api.post` not `window.api.post`)
|
|
- Check template uses correct namespace: `@click="api.post()"`
|
|
|
|
### Runtime Errors
|
|
|
|
**Error**: "@click handler not working"
|
|
|
|
**Possible Causes**:
|
|
1. Alpine not loaded
|
|
- Check browser console: `window.Alpine` should be defined
|
|
- Check main.js is loaded
|
|
- Check alpine.ts imports Alpine and starts it
|
|
|
|
2. Function not registered with Alpine
|
|
- Check source file has `Alpine.global("namespace", { ... })`
|
|
- Check namespace matches template usage
|
|
|
|
3. Template syntax error
|
|
- Check @click syntax: `@click="namespace.function()"`
|
|
- Check for typos
|
|
|
|
**Error**: "x-show not working"
|
|
|
|
**Possible Causes**:
|
|
1. Missing x-data parent
|
|
- Add `x-data="{ varName: false }"` to parent element
|
|
|
|
2. Variable name mismatch
|
|
- Check x-data variable name matches x-show variable
|
|
|
|
3. Alpine not loaded
|
|
- See above
|
|
|
|
### Template Errors
|
|
|
|
**Error**: "templ generate fails"
|
|
|
|
**Solution**:
|
|
- Check template syntax (missing closing tags, etc.)
|
|
- Check for invalid templ syntax
|
|
- Check template file encoding (UTF-8)
|
|
|
|
**Error**: "Page not rendering correctly after migration"
|
|
|
|
**Solution**:
|
|
- Check all script tags are removed except main.js
|
|
- Check main.js is loaded
|
|
- Check browser console for errors
|
|
- Check Alpine DevTools for state
|
|
- Verify onclick → @click conversion is correct
|
|
|
|
### Performance Issues
|
|
|
|
**Issue**: "Bundle size too large (>200KB)"
|
|
|
|
**Possible Causes**:
|
|
- Check if Chart.js accidentally bundled (should be CDN)
|
|
- Check if duplicate dependencies
|
|
- Run `esbuild --analyze` to see bundle contents
|
|
|
|
**Issue**: "Page load slow"
|
|
|
|
**Possible Causes**:
|
|
- Check main.js is minified in production
|
|
- Check sourcemap not loaded in production
|
|
- Check server compression enabled
|
|
- Check browser caching headers
|
|
|
|
---
|
|
|
|
## Development Workflow
|
|
|
|
### During Migration (Phases 0-2)
|
|
|
|
```bash
|
|
# Terminal 1: Watch TypeScript
|
|
npm run build:ts:watch
|
|
|
|
# Terminal 2: Watch Templates
|
|
templ generate -watch
|
|
|
|
# Terminal 3: Run Server
|
|
go run .
|
|
```
|
|
|
|
### After Migration (All Phases Complete)
|
|
|
|
```bash
|
|
# Development
|
|
npm run dev
|
|
|
|
# Production Build
|
|
npm run build
|
|
templ generate
|
|
go build
|
|
```
|
|
|
|
---
|
|
|
|
## FAQ
|
|
|
|
### Q: Why not put everything in main.ts?
|
|
|
|
**A**: Main.ts imports other modules. This keeps code:
|
|
- Organized (one file per concern)
|
|
- Maintainable (easy to find code)
|
|
- Testable (can test individual modules)
|
|
- Tree-shakeable (unused code eliminated)
|
|
|
|
### Q: Why Alpine.js instead of vanilla JS event listeners?
|
|
|
|
**A**: Alpine provides:
|
|
- Cleaner template syntax (@click vs onclick)
|
|
- Built-in state management (x-show, x-data)
|
|
- Better progressive enhancement
|
|
- SSR-friendly
|
|
- Smaller bundle than React/Vue
|
|
|
|
### Q: Why keep HTMX if we have Alpine?
|
|
|
|
**A**: They serve different purposes:
|
|
- **HTMX**: Server communication (form submissions, API calls)
|
|
- **Alpine**: Client-side state (modals, dropdowns, UI)
|
|
|
|
They work great together.
|
|
|
|
### Q: Can I use React/Vue instead of Alpine?
|
|
|
|
**A**: You could, but:
|
|
- **Larger bundle size**: React = ~40KB gzipped, Alpine = ~15KB gzipped
|
|
- **More complexity**: Need JSX compilation, more build tools
|
|
- **Overkill**: For this app's needs, Alpine is sufficient
|
|
- **HTMX synergy**: Alpine works better with HTMX
|
|
|
|
### Q: What if I need to add a new page?
|
|
|
|
**A**: Follow the dashboard pattern:
|
|
1. Create Go handler that renders template with data
|
|
2. Create .templ file with SSR data
|
|
3. Use Alpine for any client-side interactivity
|
|
4. Import TypeScript modules in main.ts
|
|
5. Register functions with Alpine if templates call them
|
|
|
|
### Q: How do I debug issues?
|
|
|
|
**A**:
|
|
1. **Browser DevTools Console**: Check for errors
|
|
2. **Network Tab**: Check main.js loads, no 404s
|
|
3. **Alpine DevTools**: Install browser extension to inspect state
|
|
4. **Sourcemaps**: Use main.js.map to debug original TypeScript
|
|
5. **Go Logs**: Check server logs for errors
|
|
|
|
---
|
|
|
|
## Glossary
|
|
|
|
- **ES Modules**: Standard JavaScript module system (import/export)
|
|
- **ESBuild**: Fast JavaScript bundler
|
|
- **Alpine.js**: Lightweight JavaScript framework for UI interactivity
|
|
- **HTMX**: Library for dynamic web pages using HTML attributes
|
|
- **Templ**: Go templating language that compiles to Go code
|
|
- **SSR**: Server-Side Rendering - server generates complete HTML
|
|
- **Progressive Enhancement**: Page works without JavaScript, enhanced with it
|
|
- **Tree-shaking**: Removing unused code from bundle
|
|
- **Sourcemap**: File that maps bundled code back to source code for debugging
|
|
- **Window globals**: Variables attached to window object (old pattern)
|
|
- **Namespace**: Grouping related functions (e.g., api.post, api.get)
|
|
|
|
---
|
|
|
|
## Appendix A: Quick Reference
|
|
|
|
### Common Patterns
|
|
|
|
**Import ES Module**:
|
|
```typescript
|
|
import { functionName } from "./module";
|
|
```
|
|
|
|
**Export from Module**:
|
|
```typescript
|
|
export { functionName1, functionName2 };
|
|
export default function mainFunction() { ... }
|
|
```
|
|
|
|
**Register with Alpine**:
|
|
```typescript
|
|
import { Alpine } from "./alpine";
|
|
|
|
Alpine.global("namespace", {
|
|
functionName1,
|
|
functionName2,
|
|
});
|
|
```
|
|
|
|
**Use in Template**:
|
|
```html
|
|
<!-- Before -->
|
|
<button onclick="namespace.functionName()">
|
|
|
|
<!-- After -->
|
|
<button @click="namespace.functionName()">
|
|
```
|
|
|
|
**Add State with Alpine**:
|
|
```html
|
|
<div x-data="{ modalOpen: false }">
|
|
<button @click="modalOpen = true">Open</button>
|
|
<div x-show="modalOpen" x-transition>
|
|
Modal content
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
---
|
|
|
|
## Appendix B: File-by-File Checklist
|
|
|
|
### Phase 0: Add ES Exports
|
|
|
|
- [ ] api.ts - Add exports
|
|
- [ ] toast.ts - Add exports
|
|
- [ ] storage.ts - Already has exports ✅
|
|
- [ ] events.ts - Add exports
|
|
- [ ] dom.ts - Already has exports ✅
|
|
- [ ] theme.ts - Add exports
|
|
- [ ] header.ts - Add exports
|
|
- [ ] woodPaneling.ts - Add exports
|
|
|
|
### Phase 1: Internal Dependencies
|
|
|
|
- [ ] dashboard.ts - Convert 10 window reads
|
|
- [ ] search.ts - No changes needed
|
|
- [ ] device-management.ts - Convert 4 window reads
|
|
- [ ] admin.ts - Convert 14 window reads
|
|
- [ ] queue.ts - Convert 8 window reads
|
|
- [ ] conflicts.ts - Convert 6 window reads
|
|
- [ ] linking.ts - Convert window reads
|
|
- [ ] custom-section-builder.ts - Convert window reads
|
|
- [ ] analytics.ts - Convert window reads
|
|
- [ ] bookshelf.ts - Convert window reads
|
|
- [ ] api-explorer.ts - Convert window reads
|
|
- [ ] library.ts - Convert 55 window reads
|
|
- [ ] collections.ts - Convert 54 window reads
|
|
- [ ] themeDropdown.ts - RESTRUCTURE to eliminate wrapping
|
|
- [ ] docs.ts - Remove window globals
|
|
|
|
### Phase 2: Alpine Registration
|
|
|
|
- [ ] api.ts - Already registered ✅
|
|
- [ ] toast.ts - Already registered ✅
|
|
- [ ] storage.ts - Add Alpine.global
|
|
- [ ] events.ts - Add Alpine.global
|
|
- [ ] dom.ts - Add Alpine.global
|
|
- [ ] theme.ts - Add Alpine.global
|
|
- [ ] header.ts - Add Alpine.global
|
|
- [ ] woodPaneling.ts - Add Alpine.global
|
|
- [ ] library.ts - Add Alpine.global
|
|
- [ ] collections.ts - Add Alpine.global
|
|
- [ ] admin.ts - Add Alpine.global
|
|
- [ ] queue.ts - Add Alpine.global
|
|
- [ ] conflicts.ts - Add Alpine.global
|
|
- [ ] docs.ts - Add Alpine.global
|
|
- [ ] search.ts - Add Alpine.global
|
|
- [ ] device-management.ts - Add Alpine.global
|
|
- [ ] password_validation.ts - Add Alpine.global
|
|
|
|
### Phase 3: Template Migration
|
|
|
|
- [ ] login.templ
|
|
- [ ] register.templ
|
|
- [ ] profile.templ
|
|
- [ ] settings.templ
|
|
- [ ] stats.templ
|
|
- [ ] sync.templ
|
|
- [ ] progress.templ
|
|
- [ ] custom_section.templ
|
|
- [ ] collection_rules.templ
|
|
- [ ] queue.templ
|
|
- [ ] conflicts.templ
|
|
- [ ] admin.templ
|
|
- [ ] admin_library.templ
|
|
- [ ] admin_users.templ
|
|
- [ ] dashboard.templ
|
|
- [ ] bookshelf.templ
|
|
- [ ] devices.templ
|
|
- [ ] analytics.templ
|
|
- [ ] api_explorer.templ
|
|
- [ ] collections.templ
|
|
- [ ] docs.templ
|
|
- [ ] header.templ
|
|
- [ ] collection_modal.templ
|
|
- [ ] restore_system_collection_modal.templ
|
|
- [ ] profile_modal.templ
|
|
- [ ] profile_form.templ
|
|
- [ ] toast.templ
|
|
|
|
### Phase 4: Data Injection
|
|
|
|
- [ ] Verify docs search still works
|
|
- [ ] Verify no data in window globals
|
|
- [ ] Document SSR-first pattern for future pages
|
|
|
|
### Phase 5: Cleanup
|
|
|
|
- [ ] Remove all old .js files
|
|
- [ ] Remove all window exports
|
|
- [ ] Update build scripts
|
|
- [ ] Final verification
|
|
- [ ] Update documentation
|
|
|
|
---
|
|
|
|
## End of Migration Plan
|
|
|
|
This plan provides a complete, incremental path from the current window-globals architecture to a modern ES modules + Alpine architecture, with clear phases, testing strategies, and rollback procedures.
|
|
|
|
**Key Points**:
|
|
- Each phase is complete and testable
|
|
- Can ship after any phase (except final cleanup)
|
|
- No legacy code remains
|
|
- Clean architecture ready for launch
|
|
- SSR-first for future pages
|
|
|
|
**Next Step**: Begin Phase 0 - Add ES module exports to utility modules.
|