Skip to content

Commit e0b1078

Browse files
committed
Adds npm start script (node env only)
1 parent f76f3d0 commit e0b1078

File tree

2 files changed

+165
-0
lines changed

2 files changed

+165
-0
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"dev": "vite dev",
88
"build": "vite build",
99
"preview": "vite preview",
10+
"start": "node start.js",
1011
"prepare": "svelte-kit sync || echo ''",
1112
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
1213
"lint": "eslint src --ext .ts,.svelte",

start.js

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* This is just a wrapper around the built SvelteKit app,
5+
* which can otherwise be started with `node build`
6+
* The purpose of this wrapper is to add:
7+
* - Automatic restarts with exponential backoff on crashes
8+
* - Graceful shutdown on SIGTERM and SIGINT
9+
* - Improved logging
10+
*/
11+
12+
import { spawn } from 'child_process';
13+
import { existsSync } from 'fs';
14+
import { join, dirname } from 'path';
15+
import { fileURLToPath } from 'url';
16+
17+
const __dirname = dirname(fileURLToPath(import.meta.url));
18+
const BUILD_DIR = join(__dirname, 'build');
19+
const ENTRY_FILE = join(BUILD_DIR, 'index.js');
20+
const MAX_RESTARTS = 10;
21+
const INITIAL_BACKOFF = 1000;
22+
const MAX_BACKOFF = 30000;
23+
24+
let restartCount = 0;
25+
let backoffTime = INITIAL_BACKOFF;
26+
let child = null;
27+
let isShuttingDown = false;
28+
29+
// Console logging with colors and icons
30+
const colors = {
31+
reset: '\x1b[0m',
32+
red: '\x1b[31m',
33+
green: '\x1b[32m',
34+
yellow: '\x1b[33m',
35+
blue: '\x1b[34m',
36+
cyan: '\x1b[36m'
37+
};
38+
39+
const log = (message, type = 'info') => {
40+
const styles = {
41+
error: { color: colors.red, icon: '❌' },
42+
success: { color: colors.green, icon: '✅' },
43+
warning: { color: colors.yellow, icon: '⚠️ ' },
44+
info: { color: colors.cyan, icon: 'ℹ️ ' },
45+
start: { color: colors.blue, icon: '🚀' },
46+
stop: { color: colors.yellow, icon: '⏹️ ' },
47+
wait: { color: colors.yellow, icon: '⏳' }
48+
};
49+
50+
const { color, icon } = styles[type] || styles.info;
51+
console.log(`${color}${icon} ${message}${colors.reset}`);
52+
};
53+
54+
// Validate build exists and is correct type
55+
function validateBuild() {
56+
if (!existsSync(BUILD_DIR)) {
57+
log('Build directory not found.', 'error');
58+
log('Run: npm run build:node', 'info');
59+
process.exit(1);
60+
}
61+
62+
if (!existsSync(ENTRY_FILE)) {
63+
log('Build entry file not found.', 'error');
64+
log('The build might not be a Node.js build.', 'warning');
65+
log('Run: npm run build:node', 'info');
66+
process.exit(1);
67+
}
68+
69+
// Check for handler.js which indicates adapter-node
70+
const handlerFile = join(BUILD_DIR, 'handler.js');
71+
if (!existsSync(handlerFile)) {
72+
log('This appears to be a static build, not a Node.js build.', 'error');
73+
log('Run: npm run build:node', 'info');
74+
process.exit(1);
75+
}
76+
}
77+
78+
function startServer() {
79+
if (isShuttingDown) return;
80+
81+
log(`Starting server... (attempt ${restartCount + 1})`, 'start');
82+
83+
child = spawn('node', ['build'], {
84+
stdio: 'inherit',
85+
env: { ...process.env, NODE_ENV: process.env.NODE_ENV || 'production' }
86+
});
87+
88+
// Reset counters after successful run (30s uptime)
89+
const successTimer = setTimeout(() => {
90+
restartCount = 0;
91+
backoffTime = INITIAL_BACKOFF;
92+
}, 30000);
93+
94+
child.on('error', (err) => {
95+
clearTimeout(successTimer);
96+
log(`Failed to start server: ${err.message}`, 'error');
97+
// Error event is always followed by exit event, so we handle restart there
98+
});
99+
100+
child.on('exit', (code, signal) => {
101+
clearTimeout(successTimer);
102+
child = null;
103+
104+
if (isShuttingDown) {
105+
log('Server stopped gracefully', 'success');
106+
process.exit(0);
107+
}
108+
109+
if (code === 0) {
110+
log('Server exited normally', 'success');
111+
process.exit(0);
112+
}
113+
114+
restartCount++;
115+
116+
if (restartCount >= MAX_RESTARTS) {
117+
log(`Server crashed ${MAX_RESTARTS} times. Giving up.`, 'error');
118+
process.exit(1);
119+
}
120+
121+
log(`Server crashed with ${signal ? `signal ${signal}` : `code ${code}`}`, 'warning');
122+
log(`Restarting in ${backoffTime / 1000}s...`, 'wait');
123+
124+
setTimeout(() => {
125+
backoffTime = Math.min(backoffTime * 2, MAX_BACKOFF);
126+
startServer();
127+
}, backoffTime);
128+
});
129+
}
130+
131+
function shutdown(signal) {
132+
if (isShuttingDown) return;
133+
isShuttingDown = true;
134+
135+
log(`Received ${signal}, shutting down gracefully...`, 'stop');
136+
137+
if (child) {
138+
child.kill('SIGTERM');
139+
140+
// Force kill after 10s if not stopped
141+
setTimeout(() => {
142+
if (child) {
143+
log('Force killing server...', 'warning');
144+
child.kill('SIGKILL');
145+
}
146+
}, 10000);
147+
} else {
148+
process.exit(0);
149+
}
150+
}
151+
152+
// Handle shutdown signals
153+
process.on('SIGTERM', () => shutdown('SIGTERM'));
154+
process.on('SIGINT', () => shutdown('SIGINT'));
155+
process.on('unhandledRejection', (err) => {
156+
log(`Unhandled rejection: ${err.message}`, 'error');
157+
shutdown('unhandledRejection');
158+
});
159+
160+
// Validate and start
161+
validateBuild();
162+
log('Node.js build detected', 'success');
163+
startServer();
164+

0 commit comments

Comments
 (0)