Ctrl K
ComunidadGuías…

scripts/start-node.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env node

import { existsSync, readFileSync, realpathSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { parseEnv } from 'node:util';

import { assertNodeVersion } from './lib/node-version.mjs';

const DEFAULT_HOST = '127.0.0.1';
const DEFAULT_PORT = 3000;
const SHUTDOWN_TIMEOUT_MS = 60_000;

export function parseStartArguments(args) {
  let envFile;
  for (let index = 0; index < args.length; index += 1) {
    const argument = args[index];
    if (argument !== '--env-file') throw new Error(`Unknown option: ${argument}`);
    const value = args[index + 1];
    if (!value || value.startsWith('--')) throw new Error('--env-file requires a file path');
    if (envFile) throw new Error('--env-file may only be provided once');
    envFile = value;
    index += 1;
  }
  if (!envFile) throw new Error('--env-file <path> is required for production startup');
  return { envFile };
}

export function applyEnvironmentFile(file, target = process.env) {
  const resolved = path.resolve(file);
  let parsed;
  try {
    parsed = parseEnv(readFileSync(resolved, 'utf8'));
  } catch (error) {
    throw new Error(`Cannot load production environment file ${resolved}: ${error.message}`, { cause: error });
  }
  for (const [key, value] of Object.entries(parsed)) {
    if (!(key in target)) target[key] = value;
  }
  return resolved;
}

export function resolveServerOptions(environment = process.env) {
  const rawHost = Object.hasOwn(environment, 'HOST') ? environment.HOST : DEFAULT_HOST;
  if (typeof rawHost !== 'string' || !rawHost || !/^[A-Za-z0-9._:-]+$/.test(rawHost)) {
    throw new Error('HOST must be a hostname or IP address without a scheme, path, or whitespace');
  }
  const rawPort = environment.PORT ?? String(DEFAULT_PORT);
  if (!/^\d+$/.test(rawPort)) throw new Error('PORT must be an integer from 1 through 65535');
  const port = Number(rawPort);
  if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) {
    throw new Error('PORT must be an integer from 1 through 65535');
  }
  return { hostname: rawHost, port };
}

export function assertProductionArtifact(file) {
  if (!existsSync(file)) {
    throw new Error(`Production Node artifact is missing at ${file}. Run npm run flue:build first.`);
  }
  return file;
}

export async function startNodeApplication({
  application,
  loadApplication,
  background,
  serverOptions,
}) {
  const ownership = background.acquireNodeProcessOwnership();
  let flueLifecycle;
  let backgroundAttempted = false;
  try {
    // A restart resumes submissions while the application assembles, so the
    // connector details must be in memory before a render can run.
    await ownership.ready?.();
    const loadedApplication = application ?? await loadApplication();
    flueLifecycle = await loadedApplication.startFlueNodeServer(serverOptions);
    backgroundAttempted = true;
    await background.startNodeBackground();
  } catch (error) {
    if (backgroundAttempted) await background.stopNodeBackground().catch(() => undefined);
    if (flueLifecycle) await flueLifecycle.stop().catch(() => undefined);
    ownership.release();
    throw error;
  }
  let stopping;
  return {
    stop() {
      if (stopping) return stopping;
      stopping = (async () => {
        const failures = [];
        try { await background.stopNodeBackground(); } catch (error) { failures.push(error); }
        try { await flueLifecycle.stop(); } catch (error) { failures.push(error); }
        try { ownership.release(); } catch (error) { failures.push(error); }
        if (failures.length === 1) throw failures[0];
        if (failures.length > 1) throw new AggregateError(failures, 'Node shutdown failed');
      })();
      return stopping;
    },
  };
}

export async function startProductionNode({ args = process.argv.slice(2) } = {}) {
  assertNodeVersion();
  const { envFile } = parseStartArguments(args);
  applyEnvironmentFile(envFile);
  const options = resolveServerOptions();
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
  const appArtifact = assertProductionArtifact(path.join(root, 'dist', 'app.mjs'));
  const backgroundArtifact = assertProductionArtifact(path.join(root, 'dist', 'node-background.mjs'));
  const background = await import(pathToFileURL(backgroundArtifact).href);
  if (typeof background.acquireNodeProcessOwnership !== 'function' ||
      typeof background.startNodeBackground !== 'function' ||
      typeof background.stopNodeBackground !== 'function') {
    throw new Error(`Production Node artifact at ${backgroundArtifact} is incomplete. Rebuild it with npm run flue:build.`);
  }
  const lifecycle = await startNodeApplication({
    loadApplication: async () => {
      const application = await import(pathToFileURL(appArtifact).href);
      if (typeof application.startFlueNodeServer !== 'function') {
        throw new Error(`Production Node artifact at ${appArtifact} does not export startFlueNodeServer. Rebuild it with npm run flue:build.`);
      }
      return application;
    },
    background,
    serverOptions: options,
  });
  // The managed tunnel must wait for this child's successful bind, not merely
  // an HTTP response that another process on the requested port could serve.
  if (process.send) process.send({ type: 'chickpea-ready' });
  let shutdown;
  const stop = (exitCode) => {
    if (shutdown) return shutdown;
    shutdown = (async () => {
      let finalExitCode = exitCode;
      const timeout = setTimeout(() => {
        console.error('[chickpea] Shutdown timed out, exiting.');
        process.exit(exitCode);
      }, SHUTDOWN_TIMEOUT_MS);
      timeout.unref();
      try {
        await lifecycle.stop();
      } catch {
        console.error('[chickpea] Graceful shutdown failed.');
        finalExitCode = 1;
      } finally {
        clearTimeout(timeout);
      }
      process.exit(finalExitCode);
    })();
    return shutdown;
  };
  process.once('SIGINT', () => { void stop(130); });
  process.once('SIGTERM', () => { void stop(143); });
  process.once('disconnect', () => { void stop(0); });
  return lifecycle;
}

export function isMainModule(argvEntry = process.argv[1], moduleUrl = import.meta.url) {
  if (!argvEntry) return false;
  try {
    return realpathSync(argvEntry) === realpathSync(fileURLToPath(moduleUrl));
  } catch {
    return false;
  }
}

const isMain = isMainModule();
if (isMain) {
  startProductionNode().catch((error) => {
    console.error(`[chickpea] ${error instanceof Error ? error.message : String(error)}`);
    process.exitCode = 1;
  });
}