|
| 1 | +#!/usr/bin/env node |
| 2 | + |
| 3 | +/** |
| 4 | + * Standalone Node.js server for TanStack Start application |
| 5 | + * This wraps the fetch handler in a simple HTTP server |
| 6 | + */ |
| 7 | + |
| 8 | +import { createServer } from 'node:http' |
| 9 | +import server from './dist/server/server.js' |
| 10 | + |
| 11 | +const PORT = process.env.PORT || 3000 |
| 12 | +const HOST = process.env.HOST || '0.0.0.0' |
| 13 | + |
| 14 | +// Create HTTP server |
| 15 | +const httpServer = createServer(async (req, res) => { |
| 16 | + try { |
| 17 | + // Build the full URL |
| 18 | + const protocol = req.connection.encrypted ? 'https' : 'http' |
| 19 | + const url = new URL(req.url, `${protocol}://${req.headers.host}`) |
| 20 | + |
| 21 | + // Convert Node.js request headers to Web Headers |
| 22 | + const headers = new Headers() |
| 23 | + for (const [key, value] of Object.entries(req.headers)) { |
| 24 | + if (value !== undefined) { |
| 25 | + if (Array.isArray(value)) { |
| 26 | + value.forEach(v => headers.append(key, v)) |
| 27 | + } else { |
| 28 | + headers.append(key, value) |
| 29 | + } |
| 30 | + } |
| 31 | + } |
| 32 | + |
| 33 | + // Get request body for non-GET/HEAD requests |
| 34 | + const body = req.method !== 'GET' && req.method !== 'HEAD' |
| 35 | + ? await new Promise((resolve, reject) => { |
| 36 | + const chunks = [] |
| 37 | + req.on('data', (chunk) => chunks.push(chunk)) |
| 38 | + req.on('end', () => resolve(Buffer.concat(chunks))) |
| 39 | + req.on('error', reject) |
| 40 | + }) |
| 41 | + : undefined |
| 42 | + |
| 43 | + // Create Web Request |
| 44 | + const request = new Request(url, { |
| 45 | + method: req.method, |
| 46 | + headers, |
| 47 | + body: body && body.length > 0 ? body : undefined, |
| 48 | + }) |
| 49 | + |
| 50 | + // Call the TanStack Start fetch handler |
| 51 | + const response = await server.fetch(request) |
| 52 | + |
| 53 | + // Convert Web Response to Node.js response |
| 54 | + res.statusCode = response.status |
| 55 | + |
| 56 | + // Copy response headers |
| 57 | + response.headers.forEach((value, key) => { |
| 58 | + res.setHeader(key, value) |
| 59 | + }) |
| 60 | + |
| 61 | + // Stream the response body |
| 62 | + if (response.body) { |
| 63 | + const reader = response.body.getReader() |
| 64 | + const pump = async () => { |
| 65 | + while (true) { |
| 66 | + const { done, value } = await reader.read() |
| 67 | + if (done) { |
| 68 | + res.end() |
| 69 | + break |
| 70 | + } |
| 71 | + if (!res.write(value)) { |
| 72 | + await new Promise(resolve => res.once('drain', resolve)) |
| 73 | + } |
| 74 | + } |
| 75 | + } |
| 76 | + await pump() |
| 77 | + } else { |
| 78 | + res.end() |
| 79 | + } |
| 80 | + } catch (error) { |
| 81 | + console.error('Server error:', error) |
| 82 | + if (!res.headersSent) { |
| 83 | + res.statusCode = 500 |
| 84 | + res.setHeader('Content-Type', 'text/plain') |
| 85 | + } |
| 86 | + if (!res.writableEnded) { |
| 87 | + res.end('Internal Server Error') |
| 88 | + } |
| 89 | + } |
| 90 | +}) |
| 91 | + |
| 92 | +httpServer.listen(PORT, HOST, () => { |
| 93 | + console.log(`✓ Server running at http://${HOST}:${PORT}`) |
| 94 | +}) |
| 95 | + |
| 96 | +// Graceful shutdown |
| 97 | +const gracefulShutdown = (signal) => { |
| 98 | + console.log(`${signal} received, closing server...`) |
| 99 | + httpServer.close(() => { |
| 100 | + console.log('Server closed') |
| 101 | + process.exit(0) |
| 102 | + }) |
| 103 | + |
| 104 | + // Force close after 10 seconds |
| 105 | + setTimeout(() => { |
| 106 | + console.error('Could not close connections in time, forcefully shutting down') |
| 107 | + process.exit(1) |
| 108 | + }, 10000) |
| 109 | +} |
| 110 | + |
| 111 | +process.on('SIGTERM', () => gracefulShutdown('SIGTERM')) |
| 112 | +process.on('SIGINT', () => gracefulShutdown('SIGINT')) |
0 commit comments