OrangeTV/scripts/dev-with-redis.js

195 lines
4.3 KiB
JavaScript

#!/usr/bin/env node
/* eslint-disable no-console */
const { spawn, spawnSync } = require('child_process');
const net = require('net');
function getRedisConfig(env = process.env) {
return {
containerName: env.REDIS_CONTAINER_NAME || 'orangetv-redis-dev',
image: env.REDIS_IMAGE || 'redis:alpine',
port: env.REDIS_PORT || '6379',
};
}
function buildRedisUrl(port) {
return `redis://localhost:${port}`;
}
function resolveRedisAction(state) {
if (!state.exists) return 'create';
if (!state.running) return 'start';
return 'none';
}
function isPortAvailable(port) {
return new Promise((resolve) => {
const server = net.createServer();
server.once('error', () => {
resolve(false);
});
server.once('listening', () => {
server.close(() => resolve(true));
});
server.listen(port);
});
}
async function assertDevPortsAvailable(ports = [3000]) {
const checks = await Promise.all(
ports.map(async (port) => ({
port,
available: await isPortAvailable(port),
}))
);
const blocked = checks.filter((check) => !check.available);
if (blocked.length > 0) {
throw new Error(
`Dev port${blocked.length > 1 ? 's are' : ' is'} already in use: ${blocked
.map((check) => check.port)
.join(', ')}.\nStop the existing OrangeTV dev server or free those ports, then retry.`
);
}
}
function runDocker(args, options = {}) {
return spawnSync('docker', args, {
encoding: 'utf8',
stdio: options.stdio || 'pipe',
});
}
function assertDockerAvailable() {
const result = runDocker(['info']);
if (result.status !== 0) {
const detail = result.stderr?.trim() || result.stdout?.trim();
throw new Error(
`Docker is not available. Start Docker Desktop and retry.\n${detail || ''}`
);
}
}
function getContainerState(containerName) {
const result = runDocker([
'inspect',
'-f',
'{{.State.Running}}',
containerName,
]);
if (result.status !== 0) {
return { exists: false, running: false };
}
return {
exists: true,
running: result.stdout.trim() === 'true',
};
}
function ensureRedis(config = getRedisConfig()) {
assertDockerAvailable();
const state = getContainerState(config.containerName);
const action = resolveRedisAction(state);
if (action === 'none') {
console.log(`Redis container "${config.containerName}" is already running.`);
return;
}
if (action === 'start') {
console.log(`Starting Redis container "${config.containerName}"...`);
const result = runDocker(['start', config.containerName], {
stdio: 'inherit',
});
if (result.status !== 0) {
throw new Error(`Failed to start Redis container "${config.containerName}".`);
}
return;
}
console.log(
`Creating Redis container "${config.containerName}" on localhost:${config.port}...`
);
const result = runDocker(
[
'run',
'--name',
config.containerName,
'-p',
`${config.port}:6379`,
'-d',
config.image,
],
{ stdio: 'inherit' }
);
if (result.status !== 0) {
throw new Error(
`Failed to create Redis container. If port ${config.port} is already in use, retry with REDIS_PORT=6380 pnpm dev:redis.`
);
}
}
function startFastifyDev(config = getRedisConfig()) {
const redisUrl = buildRedisUrl(config.port);
const env = {
...process.env,
VITE_STORAGE_TYPE: 'redis',
REDIS_URL: redisUrl,
};
console.log(`Using Redis storage: ${redisUrl}`);
console.log('Starting OrangeTV Fastify/Vite dev server...');
const child = spawn('pnpm', ['dev'], {
env,
stdio: 'inherit',
});
process.on('SIGINT', () => {
if (!child.killed) child.kill('SIGINT');
});
process.on('SIGTERM', () => {
if (!child.killed) child.kill('SIGTERM');
});
child.on('exit', (code, signal) => {
if (signal) {
process.exit(1);
}
process.exit(code || 0);
});
}
async function main() {
try {
const config = getRedisConfig();
await assertDevPortsAvailable();
ensureRedis(config);
startFastifyDev(config);
} catch (error) {
console.error(error.message);
process.exit(1);
}
}
if (require.main === module) {
main();
}
module.exports = {
assertDevPortsAvailable,
buildRedisUrl,
ensureRedis,
getRedisConfig,
isPortAvailable,
resolveRedisAction,
startFastifyDev,
};