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
		
	
			
		
		
	
	
			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));
 | |
| 		}
 | |
| 	};
 | |
| }
 |