feat(Nextjs): HMR support added & websocker added

This commit is contained in:
shiva raj badu 2025-07-20 19:50:57 +05:45
parent 3ab3756fa5
commit 8eee6d55a2
No known key found for this signature in database
15 changed files with 482 additions and 118 deletions

View file

@ -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;
}

View file

@ -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 (
<div className="flex items-center gap-2">
<Link
href="/dashboard"
prefetch={false}

View file

@ -21,11 +21,11 @@ export default function MarkdownEditor({ content, onChange }: { content: string;
});
// Update editor content when the content prop changes (for streaming)
useEffect(() => {
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 (
<div className="relative">

View file

@ -66,16 +66,11 @@ export function OutlineItem({
transform: CSS.Transform.toString(transform),
transition,
}
const handleSlideDelete = () => {
if (isStreaming) return;
dispatch(deleteSlideOutline({ index: index - 1 }))
}
return (
<div className="mb-2 bg-[#F9F9F9]">
{/* Main Title Row */}

View file

@ -43,7 +43,7 @@ const page = () => {
<div className='relative'>
<Header />
<div className='flex flex-col items-center justify-center py-8'>
<h1 className='text-3xl font-semibold font-instrument_sans'>Create Presentation </h1>
<h1 className='text-3xl font-semibold font-instrument_sans'>Create Presentation </h1>
{/* <p className='text-sm text-gray-500'>We will generate a presentation for you</p> */}
</div>

View file

@ -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

View file

@ -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 = () => {
<p className="text-gray-600 mt-2">
{layoutGroup.layouts.length} layout{layoutGroup.layouts.length !== 1 ? 's' : ''} {layoutGroup.settings.description}
</p>
</div>
</div>
</header>

View file

@ -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<string, LayoutGroup>()
const loadingGroupsCache = new Set<string>()
export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderReturn => {
const [layoutGroup, setLayoutGroup] = useState<LayoutGroup | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(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
}
}

View file

@ -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<string | null>(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
}
}

View file

@ -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<WebSocket | null>(null)
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(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
}
}

View file

@ -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 = () => {
<p className="text-gray-600 mt-2">
{layoutGroups.length} groups {layouts.length} layouts
</p>
{/* Development Mode Status */}
{process.env.NODE_ENV === 'development' && (
<div className="mt-4 flex items-center justify-center gap-2">
<div className={`flex items-center gap-2 px-3 py-1 rounded-full text-sm font-medium ${isWatcherConnected
? 'bg-green-100 text-green-800'
: 'bg-orange-100 text-orange-800'
}`}>
{isWatcherConnected ? (
<>
<Wifi className="w-4 h-4" />
Hot Reload Active
</>
) : (
<>
<WifiOff className="w-4 h-4" />
Hot Reload Disconnected
</>
)}
</div>
<Button
variant="outline"
size="sm"
onClick={retry}
className="flex items-center gap-2"
>
<RefreshCw className="w-4 h-4" />
Refresh
</Button>
</div>
)}
</div>
</div>

View file

@ -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: [

View file

@ -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"

View file

@ -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<Type1SlideLayoutProps> = ({ data: slideData }) => {
return (
<div
className=" w-full rounded-sm max-w-[1280px] shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] max-h-[720px] flex items-center aspect-video bg-white relative z-20 mx-auto"
@ -40,12 +39,12 @@ const Type1SlideLayout: React.FC<Type1SlideLayoutProps> = ({ data: slideData })
<div className="flex flex-col w-full items-start justify-center space-y-1 md:space-y-2 lg:space-y-6">
{/* Title */}
<h1 className="text-2xl sm:text-3xl lg:text-4xl xl:text-5xl font-bold text-gray-900 leading-tight">
{slideData?.title || 'Sample Title'}
{slideData?.title || ' This is the title of slide'}
</h1>
{/* Description */}
<p className="text-base sm:text-lg lg:text-xl text-gray-700 leading-relaxed">
{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.'}
</p>
</div>

View file

@ -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.');