forked from
samuel.fm/statusphere-react
the statusphere demo reworked into a vite/react app in a monorepo
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