docs: Update ESBuild migration plan documentation

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)
This commit is contained in:
2026-03-08 21:36:40 -04:00
parent 533dfd64e2
commit c0290bb106
+367 -36
View File
@@ -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<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