From 44a3751545a9daa54b31fa47498b01232b7bc125 Mon Sep 17 00:00:00 2001 From: shiva raj badu Date: Sun, 20 Jul 2025 20:29:02 +0545 Subject: [PATCH] refactor(Nexjs): websocket removed --- .../nextjs/app/layout-preview/[slug]/page.tsx | 2 +- .../hooks/useGroupLayoutLoader.ts | 94 ++++++---- .../layout-preview/hooks/useLayoutLoader.ts | 69 +++---- .../layout-preview/hooks/useLayoutWatcher.ts | 133 ------------- servers/nextjs/app/layout-preview/page.tsx | 35 +--- servers/nextjs/next.config.mjs | 37 ---- servers/nextjs/package.json | 13 +- .../default/Type1SlideLayout.tsx | 2 +- servers/nextjs/scripts/layout-watcher.cjs | 174 ------------------ 9 files changed, 103 insertions(+), 456 deletions(-) delete mode 100644 servers/nextjs/app/layout-preview/hooks/useLayoutWatcher.ts delete mode 100644 servers/nextjs/scripts/layout-watcher.cjs diff --git a/servers/nextjs/app/layout-preview/[slug]/page.tsx b/servers/nextjs/app/layout-preview/[slug]/page.tsx index f6ee9e27..00dee152 100644 --- a/servers/nextjs/app/layout-preview/[slug]/page.tsx +++ b/servers/nextjs/app/layout-preview/[slug]/page.tsx @@ -12,7 +12,7 @@ const GroupLayoutPreview = () => { const router = useRouter() const slug = params.slug as string - const { layoutGroup, loading, error, retry, isWatcherConnected } = useGroupLayoutLoader(slug) + const { layoutGroup, loading, error, retry } = useGroupLayoutLoader(slug) // Handle loading state if (loading) { diff --git a/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts b/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts index 42596224..00a14e2a 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, useCallback } from 'react' -import { useLayoutWatcher } from './useLayoutWatcher' +import { useState, useEffect, useRef } from 'react' + import { LayoutInfo, LayoutGroup, GroupedLayoutsResponse, GroupSetting } from '../types' import { toast } from 'sonner' @@ -9,33 +9,44 @@ 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 = useCallback(async () => { + 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 + } + try { setLoading(true) setError(null) + loadingGroupsCache.add(groupSlug) - const response = await fetch('/api/layouts', { - cache: 'no-cache', - headers: { 'Cache-Control': 'no-cache' } - }) - + const response = await fetch('/api/layouts') if (!response.ok) { toast.error('Error loading layouts', { description: response.statusText, }) return } - const groupedLayoutsData: GroupedLayoutsResponse[] = await response.json() // Find the specific group by slug @@ -60,8 +71,6 @@ 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) { @@ -83,9 +92,7 @@ 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, @@ -100,6 +107,31 @@ 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) + } } } @@ -115,6 +147,8 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet settings: groupSettings } + // Cache the result + layoutGroupCache.set(groupSlug, group) setLayoutGroup(group) setError(null) } @@ -124,39 +158,27 @@ 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 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(() => { + const retry = () => { + // Clear cache for this group to force reload + layoutGroupCache.delete(groupSlug) loadGroupLayouts() - }, [loadGroupLayouts]) + } useEffect(() => { if (groupSlug && !hasMountedRef.current) { hasMountedRef.current = true loadGroupLayouts() } - }, [groupSlug, loadGroupLayouts]) + }, [groupSlug]) return { layoutGroup, loading, error, - retry, - isWatcherConnected + retry } } \ 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 004fc1f0..3d4e3a2d 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, useCallback } from 'react' -import { useLayoutWatcher } from './useLayoutWatcher' +import { useState, useEffect } from 'react' + import { LayoutInfo, LayoutGroup, GroupedLayoutsResponse, GroupSetting } from '../types' import { toast } from 'sonner' @@ -10,7 +10,6 @@ interface UseLayoutLoaderReturn { loading: boolean error: string | null retry: () => void - isWatcherConnected: boolean } export const useLayoutLoader = (): UseLayoutLoaderReturn => { @@ -19,17 +18,12 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { const [loading, setLoading] = useState(true) const [error, setError] = useState(null) - const loadAllLayouts = useCallback(async () => { + const loadAllLayouts = async () => { try { setLoading(true) setError(null) - // Always fetch fresh data for layout preview - const response = await fetch('/api/layouts', { - cache: 'no-cache', - headers: { 'Cache-Control': 'no-cache' } - }) - + const response = await fetch('/api/layouts') if (!response.ok) { toast.error('Error loading layouts', { description: response.statusText, @@ -52,8 +46,6 @@ 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) { @@ -73,8 +65,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 = { @@ -92,6 +84,33 @@ 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) + } } } @@ -121,35 +140,21 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { } finally { setLoading(false) } - }, []) + } - 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(() => { + const retry = () => { loadAllLayouts() - }, [loadAllLayouts]) + } useEffect(() => { loadAllLayouts() - }, [loadAllLayouts]) + }, []) return { layoutGroups, layouts, loading, error, - retry, - isWatcherConnected + retry } } \ 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 deleted file mode 100644 index 4e3b57a8..00000000 --- a/servers/nextjs/app/layout-preview/hooks/useLayoutWatcher.ts +++ /dev/null @@ -1,133 +0,0 @@ -'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 904a94ef..a7186109 100644 --- a/servers/nextjs/app/layout-preview/page.tsx +++ b/servers/nextjs/app/layout-preview/page.tsx @@ -8,7 +8,7 @@ import { Button } from '@/components/ui/button' import { ExternalLink, Wifi, WifiOff, RefreshCw } from 'lucide-react' const LayoutPreview = () => { - const { layoutGroups, layouts, loading, error, retry, isWatcherConnected } = useLayoutLoader() + const { layoutGroups, layouts, loading, error, retry } = useLayoutLoader() const router = useRouter() // Handle loading state @@ -36,39 +36,6 @@ 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 ec366315..e19c31de 100644 --- a/servers/nextjs/next.config.mjs +++ b/servers/nextjs/next.config.mjs @@ -8,44 +8,7 @@ const __filename = fileURLToPath(import.meta.url); 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 632b9ae1..9e80adf7 100644 --- a/servers/nextjs/package.json +++ b/servers/nextjs/package.json @@ -4,14 +4,11 @@ "private": true, "type": "module", "scripts": { - "dev": "WATCHPACK_POLLING=true next dev", - "dev:with-watcher": "concurrently \"npm run dev\" \"npm run layout-watcher\"", - "layout-watcher": "node scripts/layout-watcher.cjs", + "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint", - "get:version": "next --version" - }, + "lint": "next lint" + }, "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", @@ -40,11 +37,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", diff --git a/servers/nextjs/presentation-layouts/default/Type1SlideLayout.tsx b/servers/nextjs/presentation-layouts/default/Type1SlideLayout.tsx index 6694a02e..79fd6e66 100644 --- a/servers/nextjs/presentation-layouts/default/Type1SlideLayout.tsx +++ b/servers/nextjs/presentation-layouts/default/Type1SlideLayout.tsx @@ -39,7 +39,7 @@ const Type1SlideLayout: React.FC = ({ data: slideData })
{/* Title */}

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

{/* Description */} diff --git a/servers/nextjs/scripts/layout-watcher.cjs b/servers/nextjs/scripts/layout-watcher.cjs deleted file mode 100644 index 49acdebf..00000000 --- a/servers/nextjs/scripts/layout-watcher.cjs +++ /dev/null @@ -1,174 +0,0 @@ -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