the statusphere demo reworked into a vite/react app in a monorepo
at 519630d76e5c6d7cb66b61c3c5e7f4b2a0f02a8d 281 lines 8.1 kB view raw
1#!/usr/bin/env node 2 3/** 4 * This script automatically sets up ngrok for development. 5 * It: 6 * 1. Starts ngrok to tunnel to localhost:3001 7 * 2. Gets the public HTTPS URL via ngrok's API 8 * 3. Updates the appview .env file with the ngrok URL 9 * 4. Starts both the API server and client app 10 */ 11 12const { execSync, spawn } = require('child_process') 13const fs = require('fs') 14const path = require('path') 15const http = require('http') 16const { URL } = require('url') 17 18const appviewEnvPath = path.join(__dirname, '..', 'packages', 'appview', '.env') 19const clientEnvPath = path.join(__dirname, '..', 'packages', 'client', '.env') 20 21// Check if ngrok is installed 22try { 23 execSync('ngrok --version', { stdio: 'ignore' }) 24} catch (error) { 25 console.error('❌ ngrok is not installed or not in your PATH.') 26 console.error('Please install ngrok from https://ngrok.com/download') 27 process.exit(1) 28} 29 30// Kill any existing ngrok processes 31try { 32 if (process.platform === 'win32') { 33 execSync('taskkill /f /im ngrok.exe', { stdio: 'ignore' }) 34 } else { 35 execSync('pkill -f ngrok', { stdio: 'ignore' }) 36 } 37 // Wait for processes to terminate 38 try { 39 execSync('sleep 1') 40 } catch (e) {} 41} catch (error) { 42 // If no process was found, it will throw an error, which we can ignore 43} 44 45console.log('🚀 Starting ngrok...') 46 47// Start ngrok process - now we're exposing the client (3000) instead of the API (3001) 48// This way the whole app will be served through ngrok 49const ngrokProcess = spawn('ngrok', ['http', '3000'], { 50 stdio: ['ignore', 'pipe', 'pipe'], // Allow stdout, stderr 51}) 52 53let devProcesses = null 54 55// Helper function to update .env files 56function updateEnvFile(filePath, ngrokUrl) { 57 if (!fs.existsSync(filePath)) { 58 fs.writeFileSync(filePath, '') 59 } 60 61 const content = fs.readFileSync(filePath, 'utf8') 62 63 if (filePath.includes('appview')) { 64 // Update NGROK_URL in the appview package 65 const varName = 'NGROK_URL' 66 const publicUrlName = 'PUBLIC_URL' 67 const regex = new RegExp(`^${varName}=.*$`, 'm') 68 const publicUrlRegex = new RegExp(`^${publicUrlName}=.*$`, 'm') 69 70 // Update content 71 let updatedContent = content 72 73 // Update or add NGROK_URL 74 if (regex.test(updatedContent)) { 75 updatedContent = updatedContent.replace(regex, `${varName}=${ngrokUrl}`) 76 } else { 77 updatedContent = `${updatedContent}\n${varName}=${ngrokUrl}\n` 78 } 79 80 // Update or add PUBLIC_URL - set it to the ngrok URL too 81 if (publicUrlRegex.test(updatedContent)) { 82 updatedContent = updatedContent.replace( 83 publicUrlRegex, 84 `${publicUrlName}=${ngrokUrl}`, 85 ) 86 } else { 87 updatedContent = `${updatedContent}\n${publicUrlName}=${ngrokUrl}\n` 88 } 89 90 fs.writeFileSync(filePath, updatedContent) 91 console.log( 92 `✅ Updated ${path.basename(filePath)} with ${varName}=${ngrokUrl} and ${publicUrlName}=${ngrokUrl}`, 93 ) 94 } else if (filePath.includes('client')) { 95 // For client, set VITE_API_URL to "/api" - this ensures it uses the proxy setup 96 const varName = 'VITE_API_URL' 97 const regex = new RegExp(`^${varName}=.*$`, 'm') 98 99 let updatedContent 100 if (regex.test(content)) { 101 // Update existing variable 102 updatedContent = content.replace(regex, `${varName}=/api`) 103 } else { 104 // Add new variable 105 updatedContent = `${content}\n${varName}=/api\n` 106 } 107 108 fs.writeFileSync(filePath, updatedContent) 109 console.log( 110 `✅ Updated ${path.basename(filePath)} with ${varName}=/api (proxy to API server)`, 111 ) 112 } 113} 114 115// Function to start the development servers 116function startDevServers() { 117 console.log('🚀 Starting development servers...') 118 119 // Free port 3001 if it's in use 120 try { 121 if (process.platform !== 'win32') { 122 // Kill any process using port 3001 123 execSync('kill $(lsof -t -i:3001 2>/dev/null) 2>/dev/null || true') 124 // Wait for port to be released 125 execSync('sleep 1') 126 } 127 } catch (error) { 128 // Ignore errors 129 } 130 131 // Start both servers 132 devProcesses = spawn('pnpm', ['--filter', '@statusphere/appview', 'dev'], { 133 stdio: 'inherit', 134 detached: false, 135 }) 136 137 const clientProcess = spawn( 138 'pnpm', 139 ['--filter', '@statusphere/client', 'dev'], 140 { 141 stdio: 'inherit', 142 detached: false, 143 }, 144 ) 145 146 devProcesses.on('close', (code) => { 147 console.log(`API server exited with code ${code}`) 148 killAllProcesses() 149 }) 150 151 clientProcess.on('close', (code) => { 152 console.log(`Client app exited with code ${code}`) 153 killAllProcesses() 154 }) 155} 156 157// Function to get the ngrok URL from its API 158function getNgrokUrl() { 159 return new Promise((resolve, reject) => { 160 // Wait a bit for ngrok to start its API server 161 setTimeout(() => { 162 http 163 .get('http://localhost:4040/api/tunnels', (res) => { 164 let data = '' 165 166 res.on('data', (chunk) => { 167 data += chunk 168 }) 169 170 res.on('end', () => { 171 try { 172 const tunnels = JSON.parse(data).tunnels 173 if (tunnels && tunnels.length > 0) { 174 // Find HTTPS tunnel 175 const httpsTunnel = tunnels.find((t) => t.proto === 'https') 176 if (httpsTunnel) { 177 resolve(httpsTunnel.public_url) 178 } else { 179 reject(new Error('No HTTPS tunnel found')) 180 } 181 } else { 182 reject(new Error('No tunnels found')) 183 } 184 } catch (error) { 185 reject(error) 186 } 187 }) 188 }) 189 .on('error', (err) => { 190 reject(err) 191 }) 192 }, 2000) // Give ngrok a couple seconds to start 193 }) 194} 195 196// Poll the ngrok API until we get a URL 197function pollNgrokApi() { 198 getNgrokUrl() 199 .then((ngrokUrl) => { 200 console.log(`🌍 ngrok URL: ${ngrokUrl}`) 201 202 // Update .env files with the ngrok URL 203 updateEnvFile(appviewEnvPath, ngrokUrl) 204 // We'll still call this but it will be skipped per our updated logic 205 updateEnvFile(clientEnvPath, ngrokUrl) 206 207 // Start development servers 208 startDevServers() 209 }) 210 .catch(() => { 211 // Try again in 1 second 212 setTimeout(pollNgrokApi, 1000) 213 }) 214} 215 216// Start polling after a short delay 217setTimeout(pollNgrokApi, 1000) 218 219// Handle errors 220ngrokProcess.stderr.on('data', (data) => { 221 console.error('------- NGROK ERROR -------') 222 console.error(data.toString()) 223 console.error('---------------------------') 224}) 225 226// Handle ngrok process exit 227ngrokProcess.on('close', (code) => { 228 console.log(`ngrok process exited with code ${code}`) 229 // Call our kill function to ensure everything is properly cleaned up 230 killAllProcesses() 231}) 232 233// Function to properly terminate all child processes 234function killAllProcesses() { 235 console.log('\nShutting down development environment...') 236 237 // Get ngrok process PID for force kill if needed 238 const ngrokPid = ngrokProcess.pid 239 240 // Kill main processes with a normal signal first 241 if (devProcesses) { 242 try { 243 devProcesses.kill() 244 } catch (e) {} 245 } 246 247 try { 248 ngrokProcess.kill() 249 } catch (e) {} 250 251 // Force kill ngrok if normal kill fails 252 try { 253 if (process.platform === 'win32') { 254 execSync(`taskkill /F /PID ${ngrokPid} 2>nul`, { stdio: 'ignore' }) 255 } else { 256 execSync(`kill -9 ${ngrokPid} 2>/dev/null || true`, { stdio: 'ignore' }) 257 // Also kill any remaining ngrok processes 258 execSync('pkill -9 -f ngrok 2>/dev/null || true', { stdio: 'ignore' }) 259 } 260 } catch (e) { 261 // Ignore errors if processes are already gone 262 } 263 264 // Kill any process on port 3001 to ensure clean exit 265 try { 266 if (process.platform !== 'win32') { 267 execSync('kill $(lsof -t -i:3001 2>/dev/null) 2>/dev/null || true', { 268 stdio: 'ignore', 269 }) 270 } 271 } catch (e) { 272 // Ignore errors 273 } 274 275 process.exit(0) 276} 277 278// Handle various termination signals 279process.on('SIGINT', killAllProcesses) // Ctrl+C 280process.on('SIGTERM', killAllProcesses) // Kill command 281process.on('SIGHUP', killAllProcesses) // Terminal closed