You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

140 lines
3.3 KiB
TypeScript

import { existsSync, mkdirSync, openSync, rmSync, writeFileSync } from 'fs';
import { join, dirname } from 'path';
export async function ensureWebBuilt(): Promise<void> {
const buildEntry = join('apps', 'web', 'build', 'index.js');
const isTest = String(process.env.NODE_ENV || '').toLowerCase() === 'test';
if (existsSync(buildEntry) && !isTest) return;
const lockFile = join('apps', 'web', '.build.lock');
let haveLock = false;
try {
// Intentar crear lock (exclusivo). Si existe, esperar a que termine la otra build.
openSync(lockFile, 'wx');
haveLock = true;
} catch {
// Otra build en progreso o ya hecha. Esperar hasta que exista el build.
const timeoutMs = 60_000;
const start = Date.now();
while (!existsSync(buildEntry)) {
if (Date.now() - start > timeoutMs) {
throw new Error('Timeout esperando a que termine el build de apps/web');
}
// Dormir 100ms
await new Promise((res) => setTimeout(res, 100));
}
return;
}
try {
// Asegurar carpeta build
try {
mkdirSync(dirname(buildEntry), { recursive: true });
} catch {}
// Ejecutar "bun run build" dentro de apps/web
const proc = Bun.spawn({
cmd: [process.execPath, 'run', 'build'],
cwd: join('apps', 'web'),
stdout: 'inherit',
stderr: 'inherit',
env: {
...process.env,
NODE_ENV: 'production'
}
});
const exitCode = await proc.exited;
if (exitCode !== 0) {
throw new Error(`Fallo al construir apps/web (exit ${exitCode})`);
}
} finally {
// Liberar lock
try {
rmSync(lockFile, { force: true });
} catch {}
}
}
export type RunningServer = {
baseUrl: string;
stop: () => Promise<void>;
pid: number | null;
port: number;
};
export async function startWebServer(opts: {
port?: number;
env?: Record<string, string>;
} = {}): Promise<RunningServer> {
await ensureWebBuilt();
const port = Number(opts.port || 19080);
// Lanzar servidor Node adapter: "bun ./build/index.js" en apps/web
const child = Bun.spawn({
cmd: [process.execPath, './build/index.js'],
cwd: join('apps', 'web'),
stdout: 'pipe',
stderr: 'pipe',
env: {
...process.env,
PORT: String(port),
NODE_ENV: 'test',
...(opts.env || {})
}
});
// Esperar a que esté arriba (ping a "/")
const baseUrl = `http://127.0.0.1:${port}`;
const startedAt = Date.now();
const timeoutMs = 30_000;
let lastErr: any = null;
while (Date.now() - startedAt < timeoutMs) {
try {
const res = await fetch(baseUrl + '/', { method: 'GET' });
if (res) break;
} catch (e) {
lastErr = e;
}
await new Promise((res) => setTimeout(res, 100));
}
if (Date.now() - startedAt >= timeoutMs) {
try { child.kill(); } catch {}
throw new Error(`Timeout esperando al servidor web: ${lastErr?.message || lastErr}`);
}
// Conectar logs a consola (opcional)
(async () => {
try {
for await (const chunk of child.stdout) {
try {
process.stderr.write(`[web] ${new TextDecoder().decode(chunk)}`);
} catch {}
}
} catch {}
})();
(async () => {
try {
for await (const chunk of child.stderr) {
try {
process.stderr.write(`[web] ${new TextDecoder().decode(chunk)}`);
} catch {}
}
} catch {}
})();
return {
baseUrl,
port,
pid: child.pid,
stop: async () => {
try { child.kill(); } catch {}
// Pequeña espera para liberar puerto
await new Promise((res) => setTimeout(res, 50));
}
};
}