diff --git a/ESBUILD_MIGRATION_PLAN.md b/ESBUILD_MIGRATION_PLAN.md index f343055..b43b577 100644 --- a/ESBUILD_MIGRATION_PLAN.md +++ b/ESBUILD_MIGRATION_PLAN.md @@ -366,12 +366,17 @@ import { querySelector, querySelectorAll, getElementById, createElement, showEle // Systematic replacement of all 54 instances ``` -### 1.4: Restructure Function Wrapping (Option A) +### 1.4: Restructure Function Wrapping -**web/src/themeDropdown.ts** - Eliminate 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. -**Current (WRONG)**: +--- + +#### 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(); @@ -380,64 +385,390 @@ const originalToggleThemeDropdown = (window as any).toggleThemeDropdown; }; ``` -**New Approach**: -```typescript -import { toggleThemeDropdown as originalToggle } from "./header"; -import { updateWoodPanelingIndicators } from "./woodPaneling"; +**Step-by-Step Conversion:** -export function initializeThemeDropdown() { - // Call original function - originalToggle(); - // Then update indicators +**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(); } -// Export for Alpine -export { updateThemeIndicators }; +// 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; ``` -**web/src/docs.ts** - Remove window globals +**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) -**Current (WRONG)**: +--- + +#### 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 -if (!(window as any).lunr) { ... } -const idx = (window as any).lunrIndex; -const doc = (window as any).docsData?.[result.ref]; +(window as any).toggleSidebar = toggleSidebar; ``` -**New Approach** (data already fetched via API): -```typescript -// Already using fetch in template inline JS (lines 228-285) -// Just remove window checks, fetch API handles it +This is the only window global that needs to be converted for the ESBuild migration. -// Keep only: +**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 hljs from "highlight.js"; import { Alpine } from "./alpine"; +``` -// Export for Alpine -export function initializeDocsSearch() { ... } -export { toggleSidebar }; +**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, }); ``` -### Step 1.5: Remove Window Exports from Consumer Files - -After converting all reads to imports, remove window exports from consumer files (they don't need to expose to window anymore): - -**web/src/dashboard.ts** +**3. Add ES Module Export (at end of file):** ```typescript -// Remove these lines (if present): -// (window as any).scrollCarousel = scrollCarousel; -// (window as any).openDashboardSettings = openDashboardSettings; -// ... etc +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 | 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 = + '

Search index not loaded

'; + searchResults.classList.remove("hidden"); + return; + } + + const results = idx.search(query); + + if (results.length === 0) { + searchResults.innerHTML = + '

No results found

'; + } else { + searchResults.innerHTML = results + .slice(0, 10) + .map((result: { ref: string }) => { + const doc = docsData[result.ref]; + if (!doc) return ""; + + return ` + +

${doc.title || result.ref}

+ ${doc.section ? `

${doc.section}

` : ""} +
+ `; + }) + .join(""); + } + + searchResults.classList.remove("hidden"); + } catch (error) { + console.error("Search error:", error); + searchResults.innerHTML = + '

Search error

'; + 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