feat: Add server dependencies and initial application components.
All checks were successful
Build and Release / build-and-release (push) Successful in 15m24s

This commit is contained in:
Bryan1029384756
2026-02-21 15:06:36 -06:00
parent cbda60757c
commit 84aa458012
17 changed files with 1288 additions and 12 deletions

152
deploy/server/server.js Normal file
View File

@@ -0,0 +1,152 @@
import express from 'express';
import compression from 'compression';
import { execSync } from 'child_process';
import { existsSync, mkdirSync, cpSync, rmSync, readdirSync, readFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const PORT = process.env.PORT || 3000;
const GIT_REPO_URL = process.env.GIT_REPO_URL;
const GIT_BRANCH = process.env.GIT_BRANCH || 'main';
const VITE_CONVEX_URL = process.env.VITE_CONVEX_URL;
const VITE_LIVEKIT_URL = process.env.VITE_LIVEKIT_URL;
const RELEASES_DIR = '/app/releases';
const LOADING_HTML = readFileSync(join(__dirname, 'loading.html'), 'utf-8');
let currentDistPath = null;
let building = false;
let lastBuildStatus = { status: 'idle', timestamp: null, error: null };
// --- Helpers ---
function log(msg) {
console.log(`[${new Date().toISOString()}] ${msg}`);
}
function findLatestRelease() {
if (!existsSync(RELEASES_DIR)) return null;
const entries = readdirSync(RELEASES_DIR).sort();
if (entries.length === 0) return null;
const latest = join(RELEASES_DIR, entries[entries.length - 1]);
if (existsSync(join(latest, 'index.html'))) return latest;
return null;
}
function run(cmd, opts = {}) {
log(`> ${cmd}`);
execSync(cmd, { stdio: 'inherit', timeout: 600_000, ...opts });
}
async function triggerBuild() {
if (building) return;
building = true;
lastBuildStatus = { status: 'building', timestamp: Date.now(), error: null };
const timestamp = Date.now().toString();
const buildDir = `/tmp/build-${timestamp}`;
const releaseDir = join(RELEASES_DIR, timestamp);
try {
log('Starting build...');
// Clone
run(`git clone --depth 1 --branch ${GIT_BRANCH} ${GIT_REPO_URL} ${buildDir}`);
// Install deps (web workspaces only)
run(
'npm ci --workspace=apps/web --workspace=packages/shared --workspace=packages/platform-web --include-workspace-root',
{ cwd: buildDir }
);
// Build
const env = { ...process.env, VITE_CONVEX_URL, VITE_LIVEKIT_URL };
run('npm run build:web', { cwd: buildDir, env });
// Copy dist to release dir
const distDir = join(buildDir, 'apps', 'web', 'dist');
if (!existsSync(distDir)) throw new Error('Build did not produce apps/web/dist/');
mkdirSync(releaseDir, { recursive: true });
cpSync(distDir, releaseDir, { recursive: true });
// Swap — instant, zero downtime
const oldDist = currentDistPath;
currentDistPath = releaseDir;
log(`Swapped to new release: ${releaseDir}`);
// Clean up old release
if (oldDist && existsSync(oldDist) && oldDist !== releaseDir) {
rmSync(oldDist, { recursive: true, force: true });
log(`Deleted old release: ${oldDist}`);
}
lastBuildStatus = { status: 'success', timestamp: Date.now(), error: null };
log('Build complete.');
} catch (err) {
lastBuildStatus = { status: 'failed', timestamp: Date.now(), error: err.message };
log(`Build failed: ${err.message}`);
// Clean up failed release dir if it was created
if (existsSync(releaseDir)) rmSync(releaseDir, { recursive: true, force: true });
} finally {
// Always clean up temp build dir
if (existsSync(buildDir)) rmSync(buildDir, { recursive: true, force: true });
building = false;
}
}
// --- Express App ---
const app = express();
app.use(compression());
// Webhook endpoint
app.post('/api/webhook', express.json(), (req, res) => {
if (building) return res.status(409).json({ error: 'Build already in progress' });
triggerBuild();
res.json({ message: 'Build triggered' });
});
// Status endpoint
app.get('/api/status', (_req, res) => {
res.json({ building, ...lastBuildStatus });
});
// Static file serving + SPA fallback
app.use((req, res, next) => {
// While no release exists, serve loading page
if (!currentDistPath) return res.status(503).type('html').send(LOADING_HTML);
// Serve static files from the current release
express.static(currentDistPath, {
maxAge: req.path.startsWith('/assets/') ? '1y' : 0,
immutable: req.path.startsWith('/assets/'),
index: false, // We handle index.html ourselves for SPA fallback
})(req, res, () => {
// SPA fallback: if no static file matched, serve index.html
res.sendFile(join(currentDistPath, 'index.html'));
});
});
// --- Start ---
mkdirSync(RELEASES_DIR, { recursive: true });
// Check for existing release
currentDistPath = findLatestRelease();
if (currentDistPath) {
log(`Found existing release: ${currentDistPath}`);
} else {
log('No existing release found, will build on startup.');
}
app.listen(PORT, () => {
log(`Server listening on port ${PORT}`);
// Auto-build on first start if no release exists
if (!currentDistPath && GIT_REPO_URL) {
triggerBuild();
}
});