diff --git a/nginx.conf b/nginx.conf index 9d4764aa..7198143e 100644 --- a/nginx.conf +++ b/nginx.conf @@ -16,6 +16,10 @@ http { location / { proxy_pass http://localhost:3000; + proxy_http_version 1.1; # Required for WebSocket + proxy_set_header Upgrade $http_upgrade; # WebSocket header + proxy_set_header Connection "upgrade"; # WebSocket header + proxy_set_header Host $host; proxy_read_timeout 30m; proxy_connect_timeout 30m; } diff --git a/servers/nextjs/app/(presentation-generator)/components/HeaderNab.tsx b/servers/nextjs/app/(presentation-generator)/components/HeaderNab.tsx index 5f0fbe2e..d960ad8d 100644 --- a/servers/nextjs/app/(presentation-generator)/components/HeaderNab.tsx +++ b/servers/nextjs/app/(presentation-generator)/components/HeaderNab.tsx @@ -1,5 +1,5 @@ "use client"; -import { LayoutDashboard, Settings } from "lucide-react"; +import { LayoutDashboard, Settings, Upload } from "lucide-react"; import React from "react"; import Link from "next/link"; import { RootState } from "@/store/store"; @@ -11,6 +11,7 @@ const HeaderNav = () => { return (
+ { - if (editor && content !== editor.storage.markdown.getMarkdown()) { - editor.commands.setContent(content); - } - }, [content, editor]); + // useEffect(() => { + // if (editor && content !== editor.storage.markdown.getMarkdown()) { + // editor.commands.setContent(content); + // } + // }, [content, editor]); return (
diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/OutlineItem.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/OutlineItem.tsx index 936835f0..40002f3b 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/OutlineItem.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/OutlineItem.tsx @@ -66,16 +66,11 @@ export function OutlineItem({ transform: CSS.Transform.toString(transform), transition, } - - const handleSlideDelete = () => { if (isStreaming) return; dispatch(deleteSlideOutline({ index: index - 1 })) } - - - return (
{/* Main Title Row */} diff --git a/servers/nextjs/app/(presentation-generator)/upload/page.tsx b/servers/nextjs/app/(presentation-generator)/upload/page.tsx index 13d3c028..0866a9a8 100644 --- a/servers/nextjs/app/(presentation-generator)/upload/page.tsx +++ b/servers/nextjs/app/(presentation-generator)/upload/page.tsx @@ -43,7 +43,7 @@ const page = () => {
-

Create Presentation

+

Create Presentation

{/*

We will generate a presentation for you

*/}
diff --git a/servers/nextjs/app/api/layouts/route.ts b/servers/nextjs/app/api/layouts/route.ts index 9ffc3ce1..a179122e 100644 --- a/servers/nextjs/app/api/layouts/route.ts +++ b/servers/nextjs/app/api/layouts/route.ts @@ -3,7 +3,6 @@ import { promises as fs } from 'fs' import path from 'path' import { GroupSetting } from '@/app/layout-preview/types' - export async function GET() { try { // Get the path to the presentation-layouts directory diff --git a/servers/nextjs/app/layout-preview/[slug]/page.tsx b/servers/nextjs/app/layout-preview/[slug]/page.tsx index af64dd0d..f6ee9e27 100644 --- a/servers/nextjs/app/layout-preview/[slug]/page.tsx +++ b/servers/nextjs/app/layout-preview/[slug]/page.tsx @@ -5,14 +5,14 @@ import { useGroupLayoutLoader } from '../hooks/useGroupLayoutLoader' import LoadingStates from '../components/LoadingStates' import { Card } from '@/components/ui/card' import { Button } from '@/components/ui/button' -import { ArrowLeft, Home } from 'lucide-react' +import { ArrowLeft, Home, Wifi, WifiOff, RefreshCw } from 'lucide-react' const GroupLayoutPreview = () => { const params = useParams() const router = useRouter() const slug = params.slug as string - const { layoutGroup, loading, error, retry } = useGroupLayoutLoader(slug) + const { layoutGroup, loading, error, retry, isWatcherConnected } = useGroupLayoutLoader(slug) // Handle loading state if (loading) { @@ -63,6 +63,7 @@ const GroupLayoutPreview = () => {

{layoutGroup.layouts.length} layout{layoutGroup.layouts.length !== 1 ? 's' : ''} β€’ {layoutGroup.settings.description}

+
diff --git a/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts b/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts index 00a14e2a..42596224 100644 --- a/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts +++ b/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts @@ -1,6 +1,6 @@ 'use client' -import { useState, useEffect, useRef } from 'react' - +import { useState, useEffect, useRef, useCallback } from 'react' +import { useLayoutWatcher } from './useLayoutWatcher' import { LayoutInfo, LayoutGroup, GroupedLayoutsResponse, GroupSetting } from '../types' import { toast } from 'sonner' @@ -9,44 +9,33 @@ interface UseGroupLayoutLoaderReturn { loading: boolean error: string | null retry: () => void + isWatcherConnected: boolean } -// Global cache to store layout groups and avoid re-fetching -const layoutGroupCache = new Map() -const loadingGroupsCache = new Set() - export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderReturn => { const [layoutGroup, setLayoutGroup] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + const hasMountedRef = useRef(false) - const loadGroupLayouts = async () => { - // Check cache first - if (layoutGroupCache.has(groupSlug)) { - setLayoutGroup(layoutGroupCache.get(groupSlug)!) - setLoading(false) - setError(null) - return - } - - // Prevent multiple simultaneous requests for the same group - if (loadingGroupsCache.has(groupSlug)) { - return - } - + const loadGroupLayouts = useCallback(async () => { try { setLoading(true) setError(null) - loadingGroupsCache.add(groupSlug) - const response = await fetch('/api/layouts') + const response = await fetch('/api/layouts', { + cache: 'no-cache', + headers: { 'Cache-Control': 'no-cache' } + }) + if (!response.ok) { toast.error('Error loading layouts', { description: response.statusText, }) return } + const groupedLayoutsData: GroupedLayoutsResponse[] = await response.json() // Find the specific group by slug @@ -71,6 +60,8 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet for (const fileName of targetGroupData.files) { try { const layoutName = fileName.replace('.tsx', '').replace('.ts', '') + + // Import the module to get exports const module = await import(`@/presentation-layouts/${targetGroupData.groupName}/${layoutName}`) if (!module.default) { @@ -92,7 +83,9 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet // Use empty object to let schema apply its default values const sampleData = module.Schema.parse({}) const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '') - + + + const layoutInfo: LayoutInfo = { name: layoutName, component: module.default, @@ -107,31 +100,6 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet } catch (importError) { console.error(`Failed to import ${fileName} from ${targetGroupData.groupName}:`, importError) - - // Try alternative import path - try { - const layoutName = fileName.replace('.tsx', '').replace('.ts', '') - const module = await import(`@/presentation-layouts/${targetGroupData.groupName}/${layoutName}`) - - if (module.default && module.Schema) { - const sampleData = module.Schema.parse({}) - const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '') - const layoutInfo: LayoutInfo = { - name: layoutName, - component: module.default, - schema: module.Schema, - sampleData, - fileName, - groupName: targetGroupData.groupName, - layoutId - } - groupLayouts.push(layoutInfo) - } else { - console.error(`${layoutName} is missing required exports (default component or Schema)`) - } - } catch (altError) { - console.error(`Alternative import also failed for ${fileName} from ${targetGroupData.groupName}:`, altError) - } } } @@ -147,8 +115,6 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet settings: groupSettings } - // Cache the result - layoutGroupCache.set(groupSlug, group) setLayoutGroup(group) setError(null) } @@ -158,27 +124,39 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet setError(error instanceof Error ? error.message : 'Failed to load group layouts') } finally { setLoading(false) - loadingGroupsCache.delete(groupSlug) } - } + }, [groupSlug]) - const retry = () => { - // Clear cache for this group to force reload - layoutGroupCache.delete(groupSlug) + const handleLayoutChange = useCallback(() => { + console.log(`πŸ”„ Layout change detected for group: ${groupSlug}, reloading...`) + + setTimeout(() => { + loadGroupLayouts() + }, 150) + }, [loadGroupLayouts]) + + // Setup file watcher for development + const { isConnected: isWatcherConnected } = useLayoutWatcher({ + onLayoutChange: handleLayoutChange, + enabled: process.env.NODE_ENV === 'development' + }) + + const retry = useCallback(() => { loadGroupLayouts() - } + }, [loadGroupLayouts]) useEffect(() => { if (groupSlug && !hasMountedRef.current) { hasMountedRef.current = true loadGroupLayouts() } - }, [groupSlug]) + }, [groupSlug, loadGroupLayouts]) return { layoutGroup, loading, error, - retry + retry, + isWatcherConnected } } \ No newline at end of file diff --git a/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts b/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts index 3d4e3a2d..004fc1f0 100644 --- a/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts +++ b/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts @@ -1,6 +1,6 @@ 'use client' -import { useState, useEffect } from 'react' - +import { useState, useEffect, useCallback } from 'react' +import { useLayoutWatcher } from './useLayoutWatcher' import { LayoutInfo, LayoutGroup, GroupedLayoutsResponse, GroupSetting } from '../types' import { toast } from 'sonner' @@ -10,6 +10,7 @@ interface UseLayoutLoaderReturn { loading: boolean error: string | null retry: () => void + isWatcherConnected: boolean } export const useLayoutLoader = (): UseLayoutLoaderReturn => { @@ -18,12 +19,17 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { const [loading, setLoading] = useState(true) const [error, setError] = useState(null) - const loadAllLayouts = async () => { + const loadAllLayouts = useCallback(async () => { try { setLoading(true) setError(null) - const response = await fetch('/api/layouts') + // Always fetch fresh data for layout preview + const response = await fetch('/api/layouts', { + cache: 'no-cache', + headers: { 'Cache-Control': 'no-cache' } + }) + if (!response.ok) { toast.error('Error loading layouts', { description: response.statusText, @@ -46,6 +52,8 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { for (const fileName of groupData.files) { try { const layoutName = fileName.replace('.tsx', '').replace('.ts', '') + + // Always fresh import for hot reloading - no caching const module = await import(`@/presentation-layouts/${groupData.groupName}/${layoutName}`) if (!module.default) { @@ -65,8 +73,8 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { } // Use empty object to let schema apply its default values - // User will need to provide actual data when using the layouts const sampleData = module.Schema.parse({}) + console.log('πŸ” Sample data:', sampleData) const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '') const layoutInfo: LayoutInfo = { @@ -84,33 +92,6 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { } catch (importError) { console.error(`Failed to import ${fileName} from ${groupData.groupName}:`, importError) - - // Try alternative import path - try { - const layoutName = fileName.replace('.tsx', '').replace('.ts', '') - const module = await import(`@/presentation-layouts/${groupData.groupName}/${layoutName}`) - - if (module.default && module.Schema) { - // Use empty object to let schema apply its default values - const sampleData = module.Schema.parse({}) - const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '') - const layoutInfo: LayoutInfo = { - name: layoutName, - component: module.default, - schema: module.Schema, - sampleData, - fileName, - groupName: groupData.groupName, - layoutId - } - groupLayouts.push(layoutInfo) - allLayouts.push(layoutInfo) - } else { - console.error(`${layoutName} is missing required exports (default component or Schema)`) - } - } catch (altError) { - console.error(`Alternative import also failed for ${fileName} from ${groupData.groupName}:`, altError) - } } } @@ -140,21 +121,35 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { } finally { setLoading(false) } - } + }, []) - const retry = () => { + const handleLayoutChange = () => { + console.log('πŸ”„ Layout change detected, reloading...') + setTimeout(() => { + loadAllLayouts() + }, 150) + }; + + // Setup file watcher for development + const { isConnected: isWatcherConnected } = useLayoutWatcher({ + onLayoutChange: handleLayoutChange, + enabled: process.env.NODE_ENV === 'development' + }) + + const retry = useCallback(() => { loadAllLayouts() - } + }, [loadAllLayouts]) useEffect(() => { loadAllLayouts() - }, []) + }, [loadAllLayouts]) return { layoutGroups, layouts, loading, error, - retry + retry, + isWatcherConnected } } \ No newline at end of file diff --git a/servers/nextjs/app/layout-preview/hooks/useLayoutWatcher.ts b/servers/nextjs/app/layout-preview/hooks/useLayoutWatcher.ts new file mode 100644 index 00000000..4e3b57a8 --- /dev/null +++ b/servers/nextjs/app/layout-preview/hooks/useLayoutWatcher.ts @@ -0,0 +1,133 @@ +'use client' +import { useEffect, useRef, useState } from 'react' +import { toast } from 'sonner' + +interface UseLayoutWatcherProps { + onLayoutChange: () => void + enabled?: boolean +} + +export const useLayoutWatcher = ({ onLayoutChange, enabled = true }: UseLayoutWatcherProps) => { + const wsRef = useRef(null) + const reconnectTimeoutRef = useRef(null) + const [isConnected, setIsConnected] = useState(false) + + const connectWebSocket = () => { + if (!enabled || typeof window === 'undefined') return + + try { + // Get the current host (works in Docker and local development) + const host = window.location.hostname + const wsUrl = `ws://${host}:3001` + + console.log('πŸ”„ Connecting to layout watcher:', wsUrl) + + // Create WebSocket connection to our layout watcher endpoint + const ws = new WebSocket(wsUrl) + wsRef.current = ws + + ws.onopen = () => { + console.log('βœ… Layout watcher connected successfully') + setIsConnected(true) + + // Clear any existing reconnect timeout + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current) + reconnectTimeoutRef.current = null + } + } + + ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data) + console.log('πŸ“¨ WebSocket message received:', data) + + if (data.type === 'layout-change') { + console.log('πŸ”₯ Layout file changed:', data.file) + console.log('πŸ“ Group:', data.groupName, 'File:', data.fileName) + + // Show toast notification + toast.success('Layout updated!', { + description: `${data.file} has been reloaded`, + duration: 2000, + }) + + // Trigger reload + console.log('πŸš€ Triggering layout reload...') + onLayoutChange() + } else if (data.type === 'connected') { + console.log('βœ… Layout watcher handshake complete') + } + } catch (error) { + console.error('❌ Error parsing WebSocket message:', error) + } + } + + ws.onclose = (event) => { + console.log('πŸ”Œ Layout watcher disconnected. Code:', event.code, 'Reason:', event.reason) + setIsConnected(false) + wsRef.current = null + + // Attempt to reconnect after 3 seconds if enabled + if (enabled) { + console.log('⏰ Will attempt to reconnect in 3 seconds...') + reconnectTimeoutRef.current = setTimeout(() => { + console.log('πŸ”„ Attempting to reconnect layout watcher...') + connectWebSocket() + }, 3000) + } + } + + ws.onerror = (error) => { + console.error('❌ Layout watcher WebSocket error:', error) + setIsConnected(false) + ws.close() + } + + } catch (error) { + console.error('❌ Failed to create WebSocket connection:', error) + setIsConnected(false) + } + } + + useEffect(() => { + if (enabled) { + console.log('🎯 Layout watcher hook enabled, connecting...') + connectWebSocket() + } else { + console.log('⏸️ Layout watcher hook disabled') + } + + return () => { + if (wsRef.current) { + console.log('🧹 Cleaning up WebSocket connection') + wsRef.current.close() + wsRef.current = null + } + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current) + reconnectTimeoutRef.current = null + } + setIsConnected(false) + } + }, [enabled]) + + // Cleanup on component unmount + useEffect(() => { + return () => { + console.log('🧹 Cleaning up WebSocket connection') + if (wsRef.current) { + wsRef.current.close() + } + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current) + } + setIsConnected(false) + } + }, []) + + return { + isConnected, + reconnect: connectWebSocket + } +} \ No newline at end of file diff --git a/servers/nextjs/app/layout-preview/page.tsx b/servers/nextjs/app/layout-preview/page.tsx index 91406dca..904a94ef 100644 --- a/servers/nextjs/app/layout-preview/page.tsx +++ b/servers/nextjs/app/layout-preview/page.tsx @@ -5,10 +5,10 @@ import { useLayoutLoader } from './hooks/useLayoutLoader' import LoadingStates from './components/LoadingStates' import { Card } from '@/components/ui/card' import { Button } from '@/components/ui/button' -import { ExternalLink } from 'lucide-react' +import { ExternalLink, Wifi, WifiOff, RefreshCw } from 'lucide-react' const LayoutPreview = () => { - const { layoutGroups, layouts, loading, error, retry } = useLayoutLoader() + const { layoutGroups, layouts, loading, error, retry, isWatcherConnected } = useLayoutLoader() const router = useRouter() // Handle loading state @@ -35,6 +35,40 @@ const LayoutPreview = () => {

{layoutGroups.length} groups β€’ {layouts.length} layouts

+ + {/* Development Mode Status */} + {process.env.NODE_ENV === 'development' && ( +
+
+ {isWatcherConnected ? ( + <> + + Hot Reload Active + + ) : ( + <> + + Hot Reload Disconnected + + )} +
+ + +
+ )} + +
diff --git a/servers/nextjs/next.config.mjs b/servers/nextjs/next.config.mjs index 5338685f..ec366315 100644 --- a/servers/nextjs/next.config.mjs +++ b/servers/nextjs/next.config.mjs @@ -1,6 +1,51 @@ +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); + + /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: false, + + // Enable experimental features for better HMR + experimental: { + optimizePackageImports: ['presentation-layouts'], + }, + + // Webpack configuration for better hot reloading + webpack: (config, { dev, isServer }) => { + if (dev && !isServer) { + // Enable hot reloading for presentation layouts + config.watchOptions = { + ...config.watchOptions, + ignored: /node_modules/, + poll: 1000, + aggregateTimeout: 300, + }; + + // Add alias for presentation layouts + config.resolve.alias = { + ...config.resolve.alias, + '@/presentation-layouts': path.join(process.cwd(), 'presentation-layouts'), + }; + + // Configure module resolution for better hot reloading + config.resolve.symlinks = false; + config.resolve.cache = false; + + // Optimize for faster rebuilds + config.cache = { + type: 'filesystem', + buildDependencies: { + config: [__filename], + }, + cacheDirectory: path.join(process.cwd(), '.next/cache/webpack'), + }; + } + + return config; + }, images: { remotePatterns: [ diff --git a/servers/nextjs/package.json b/servers/nextjs/package.json index f8d58538..632b9ae1 100644 --- a/servers/nextjs/package.json +++ b/servers/nextjs/package.json @@ -4,7 +4,9 @@ "private": true, "type": "module", "scripts": { - "dev": "next dev", + "dev": "WATCHPACK_POLLING=true next dev", + "dev:with-watcher": "concurrently \"npm run dev\" \"npm run layout-watcher\"", + "layout-watcher": "node scripts/layout-watcher.cjs", "build": "next build", "start": "next start", "lint": "next lint", @@ -38,9 +40,11 @@ "@tiptap/extension-underline": "^2.0.0", "@tiptap/react": "^2.11.5", "@tiptap/starter-kit": "^2.11.5", + "chokidar": "^4.0.1", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.0", + "concurrently": "^9.1.0", "jsonrepair": "^3.12.0", "lucide-react": "^0.447.0", "marked": "^15.0.11", @@ -57,6 +61,7 @@ "tailwind-scrollbar-hide": "^2.0.0", "tailwindcss-animate": "^1.0.7", "tiptap-markdown": "^0.8.10", + "ws": "^8.18.0", "zod": "^4.0.5" }, "devDependencies": { @@ -67,6 +72,7 @@ "@types/react-dom": "^18", "@types/sharp": "^0.32.0", "@types/uuid": "^10.0.0", + "@types/ws": "^8.5.13", "cypress": "^14.3.3", "tailwindcss": "^3.4.1", "typescript": "^5" diff --git a/servers/nextjs/presentation-layouts/default/Type1SlideLayout.tsx b/servers/nextjs/presentation-layouts/default/Type1SlideLayout.tsx index 888adb91..6694a02e 100644 --- a/servers/nextjs/presentation-layouts/default/Type1SlideLayout.tsx +++ b/servers/nextjs/presentation-layouts/default/Type1SlideLayout.tsx @@ -7,10 +7,10 @@ export const layoutName = 'Type1 Slide' export const layoutDescription = 'A clean two-column layout with title and description on the left and a featured image on the right.' const type1SlideSchema = z.object({ - title: z.string().min(3).max(100).default('Sample Title').meta({ + title: z.string().min(3).max(100).default('Hot NOT Reload Working!').meta({ description: "Main title of the slide", }), - description: z.string().min(10).max(500).default('Your description content goes here. This layout provides a clean and professional way to present content with supporting imagery.').meta({ + description: z.string().min(10).max(500).default('This is a test of the hot reload system! If you can see this text, hot reload is working perfectly. Changes should appear instantly without page refresh.').meta({ description: "Main description text", }), image: ImageSchema.default({ @@ -30,7 +30,6 @@ interface Type1SlideLayoutProps { } const Type1SlideLayout: React.FC = ({ data: slideData }) => { - return (
= ({ data: slideData })
{/* Title */}

- {slideData?.title || 'Sample Title'} + {slideData?.title || ' This is the title of slide'}

{/* Description */}

- {slideData?.description || 'Your description content goes here. This layout provides a clean and professional way to present content with supporting imagery.'} + {slideData?.description || 'This is a test of the hot reload system! If you can see this text, hot reload is working perfectly. Changes should appear instantly without page refresh.'}

diff --git a/servers/nextjs/scripts/layout-watcher.cjs b/servers/nextjs/scripts/layout-watcher.cjs new file mode 100644 index 00000000..49acdebf --- /dev/null +++ b/servers/nextjs/scripts/layout-watcher.cjs @@ -0,0 +1,174 @@ +const WebSocket = require('ws'); +const chokidar = require('chokidar'); +const path = require('path'); + +const PORT = 3001; +const LAYOUTS_DIR = path.join(process.cwd(), 'presentation-layouts'); + +console.log('πŸ” Starting Layout Watcher...'); +console.log('πŸ“ Watching directory:', LAYOUTS_DIR); + +// Create WebSocket server (without path since ws doesn't handle paths like HTTP) +const wss = new WebSocket.Server({ + port: PORT +}); + +console.log(`πŸ”Œ WebSocket server running on ws://localhost:${PORT}`); + +// Track connected clients +let connectedClients = new Set(); + +wss.on('connection', (ws) => { + console.log('πŸ‘‹ Client connected to layout watcher'); + connectedClients.add(ws); + + ws.on('close', () => { + console.log('πŸ‘‹ Client disconnected from layout watcher'); + connectedClients.delete(ws); + }); + + ws.on('error', (error) => { + console.error('❌ WebSocket error:', error); + connectedClients.delete(ws); + }); + + // Send initial connection confirmation + ws.send(JSON.stringify({ + type: 'connected', + message: 'Layout watcher connected', + timestamp: Date.now() + })); +}); + +// File watcher setup +const watcher = chokidar.watch(LAYOUTS_DIR, { + ignored: [ + /(^|[\/\\])\../, // ignore dotfiles + /node_modules/, + /\.git/, + /\.next/, + /\.DS_Store/, + /thumbs\.db/i + ], + persistent: true, + ignoreInitial: true, // Don't fire events for initial scan + followSymlinks: false, + depth: 3, // Limit depth to avoid deep nested directories + awaitWriteFinish: { + stabilityThreshold: 100, // Wait 100ms after last write + pollInterval: 50 + } +}); + +// Debounce function to prevent rapid fire events +const debounce = (func, wait) => { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +}; + +// Broadcast changes to all connected clients +const broadcastChange = debounce((eventType, filePath) => { + if (connectedClients.size === 0) return; + + const relativePath = path.relative(LAYOUTS_DIR, filePath); + const pathParts = relativePath.split(path.sep); + + // Only process .tsx and .ts files in group directories + if (pathParts.length >= 2 && (filePath.endsWith('.tsx') || filePath.endsWith('.ts'))) { + const groupName = pathParts[0]; + const fileName = pathParts[pathParts.length - 1]; + + // Skip non-layout files + if (fileName.startsWith('.') || fileName.includes('.test.') || fileName.includes('.spec.')) { + return; + } + + const changeData = { + type: 'layout-change', + eventType, + file: relativePath, + groupName, + fileName, + fullPath: filePath, + timestamp: Date.now() + }; + + console.log(`πŸ”₯ Broadcasting ${eventType}:`, relativePath); + + // Send to all connected clients + connectedClients.forEach(ws => { + if (ws.readyState === WebSocket.OPEN) { + try { + ws.send(JSON.stringify(changeData)); + } catch (error) { + console.error('Error sending WebSocket message:', error); + connectedClients.delete(ws); + } + } else { + // Remove disconnected clients + connectedClients.delete(ws); + } + }); + } +}, 200); // Debounce for 200ms + +// Watch for file changes +watcher + .on('change', (filePath) => { + console.log('πŸ“ File changed:', path.relative(LAYOUTS_DIR, filePath)); + broadcastChange('change', filePath); + }) + .on('add', (filePath) => { + console.log('βž• File added:', path.relative(LAYOUTS_DIR, filePath)); + broadcastChange('add', filePath); + }) + .on('unlink', (filePath) => { + console.log('πŸ—‘οΈ File removed:', path.relative(LAYOUTS_DIR, filePath)); + broadcastChange('unlink', filePath); + }) + .on('addDir', (dirPath) => { + console.log('πŸ“ Directory added:', path.relative(LAYOUTS_DIR, dirPath)); + broadcastChange('addDir', dirPath); + }) + .on('unlinkDir', (dirPath) => { + console.log('πŸ“ Directory removed:', path.relative(LAYOUTS_DIR, dirPath)); + broadcastChange('unlinkDir', dirPath); + }) + .on('error', (error) => { + console.error('❌ Watcher error:', error); + }) + .on('ready', () => { + console.log('βœ… Initial scan complete. Ready for changes.'); + console.log(`πŸ“Š Watching ${Object.keys(watcher.getWatched()).length} directories`); + }); + +// Graceful shutdown +process.on('SIGINT', () => { + console.log('\nπŸ›‘ Shutting down layout watcher...'); + watcher.close().then(() => { + console.log('πŸ“ File watcher closed'); + wss.close(() => { + console.log('πŸ”Œ WebSocket server closed'); + process.exit(0); + }); + }); +}); + +process.on('SIGTERM', () => { + console.log('\nπŸ›‘ Received SIGTERM, shutting down...'); + watcher.close().then(() => { + wss.close(() => { + process.exit(0); + }); + }); +}); + +// Keep the process alive +console.log('πŸš€ Layout watcher is running. Press Ctrl+C to stop.'); \ No newline at end of file