Ctrl K
ComunidadGuías…

vite.config.js

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

Archivo original ↗Preguntar a KiBot
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
import { spawn } from 'child_process';
import { readFileSync, statSync } from 'fs';
import { join, resolve } from 'path';
import { fileURLToPath } from 'url';
import { getSyncRun, getSyncStatus, startSyncRun } from './sync/sync-runner.mjs';
import { readLedgerLoopRuns } from './sync/loop-ledger-to-runs.mjs';
import { querySessionHistory } from './sync/session-history.mjs';
import { publicDemoAssets } from './sync/build-public-assets.mjs';
import { readSnapshot } from './sync/snapshot-generation.mjs';
import { loadEnv } from './sync/load-env.mjs';

const __dirname = fileURLToPath(new URL('.', import.meta.url));
loadEnv(__dirname);

// Local-only dev plugin: exposes the same observable background sync contract
// as the localhost helper.
function meowSyncPlugin() {
  return {
    name: 'meow-sync',
    configureServer(server) {
      // Local-only guard. These endpoints spawn local processes, so they must
      // never be reachable from another site. Reject non-localhost Host headers
      // (DNS-rebinding) and any cross-origin request. Without this, a page the
      // developer visits during `npm run dev` could POST /api/sync.
      function blockNonLocal(req, res) {
        const address = server.httpServer?.address();
        const activePort = typeof address === 'object' && address
          ? address.port
          : Number(server.config.server.port || 5173);
        const isLoopback = (hostname) => ['localhost', '127.0.0.1', '::1']
          .includes(hostname.replace(/^\[|\]$/g, '').toLowerCase());
        const portOf = (url) => Number(url.port || (url.protocol === 'https:' ? 443 : 80));
        let okHost = false;
        try {
          const host = new URL(`http://${req.headers.host || ''}`);
          okHost = isLoopback(host.hostname) && portOf(host) === activePort;
        } catch { /* Reject malformed Host headers. */ }
        let okOrigin = true;
        if (req.headers.origin) {
          try {
            const origin = new URL(req.headers.origin);
            const protocol = server.config.server.https ? 'https:' : 'http:';
            okOrigin = origin.origin === req.headers.origin
              && origin.protocol === protocol
              && isLoopback(origin.hostname)
              && portOf(origin) === activePort;
          } catch { okOrigin = false; }
        }
        if (!okHost || !okOrigin) {
          res.statusCode = 403;
          res.setHeader('Content-Type', 'application/json');
          res.end(JSON.stringify({ ok: false, error: 'forbidden (local-only endpoint)' }));
          return true;
        }
        return false;
      }

      server.middlewares.use('/api/sync/status', (req, res) => {
        if (req.method !== 'GET') {
          res.statusCode = 405;
          res.end();
          return;
        }
        if (blockNonLocal(req, res)) return;
        res.setHeader('Content-Type', 'application/json');
        res.end(JSON.stringify(getSyncStatus({ repoRoot: server.config.root })));
      });

      server.middlewares.use('/api/sync/runs', (req, res) => {
        if (req.method !== 'GET') { res.statusCode = 405; res.end(); return; }
        if (blockNonLocal(req, res)) return;
        const runId = new URL(req.url || '/', 'http://localhost').pathname.split('/').filter(Boolean)[0];
        const run = getSyncRun(runId);
        res.statusCode = run ? 200 : 404;
        res.setHeader('Content-Type', 'application/json');
        res.end(JSON.stringify(run || { ok: false, error: 'Sync run not found' }));
      });

      server.middlewares.use('/api/data/snapshot.json', (req, res) => {
        if (req.method !== 'GET') { res.statusCode = 405; res.end(); return; }
        if (blockNonLocal(req, res)) return;
        res.setHeader('Content-Type', 'application/json');
        res.setHeader('Cache-Control', 'no-store');
        try {
          const snapshot = readSnapshot(process.env.MEOW_DATA_DIR || join(server.config.root, 'public', 'data'));
          res.statusCode = snapshot ? 200 : 404;
          res.end(JSON.stringify(snapshot || { error: 'No generation published yet.' }));
        } catch {
          res.statusCode = 503;
          res.end(JSON.stringify({ error: 'Local snapshot validation failed.' }));
        }
      });

      server.middlewares.use('/api/session-history/sessions', (req, res) => {
        if (req.method !== 'GET') { res.statusCode = 405; res.end(); return; }
        if (blockNonLocal(req, res)) return;
        try {
          const url = new URL(req.url || '/', 'http://localhost');
          const result = querySessionHistory({
            limit: url.searchParams.get('limit'),
            cursor: url.searchParams.get('cursor'),
            from: url.searchParams.get('from'),
            to: url.searchParams.get('to'),
            project: url.searchParams.get('project'),
            source: url.searchParams.get('source'),
            model: url.searchParams.get('model'),
            expectedVersion: url.searchParams.get('expectedVersion'),
            snapshotBytes: url.searchParams.get('snapshotBytes'),
          });
          res.setHeader('Content-Type', 'application/json');
          res.end(JSON.stringify(result));
        } catch (err) {
          res.statusCode = err?.code === 'stale_archive_snapshot' ? 409 : 500;
          res.setHeader('Content-Type', 'application/json');
          res.end(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
        }
      });

      // Dev-mode mirror of the local API's Loop-Ops endpoints (sync/local-api.mjs).
      // Spec is served statically; runs/gates need a local fallback when their
      // gitignored JSON files do not exist.
      for (const [route, file, fallback] of [
        ['/api/loop-ops/runs', 'runs.json', () => readLedgerLoopRuns()],
        ['/api/loop-ops/gates', 'gates.json', () => []],
      ]) {
        server.middlewares.use(route, (req, res) => {
          if (req.method !== 'GET') { res.statusCode = 405; res.end(); return; }
          if (blockNonLocal(req, res)) return;
          let payload;
          try {
            payload = JSON.parse(readFileSync(join(server.config.root, 'public', 'data', 'loop-ops', file), 'utf8'));
          } catch {
            payload = fallback();
          }
          res.setHeader('Content-Type', 'application/json');
          res.end(JSON.stringify(payload));
        });
      }

      server.middlewares.use('/api/loop-ops/status', (req, res) => {
        if (req.method !== 'GET') { res.statusCode = 405; res.end(); return; }
        if (blockNonLocal(req, res)) return;
        const dir = join(server.config.root, 'public', 'data', 'loop-ops');
        const files = {};
        for (const name of ['spec.json', 'gates.json', 'runs.json']) {
          try {
            const st = statSync(join(dir, name));
            files[name] = { mtime: st.mtimeMs, size: st.size };
          } catch { files[name] = null; }
        }
        res.setHeader('Content-Type', 'application/json');
        res.end(JSON.stringify({ ok: files['spec.json'] !== null, files, productionWritesEnabled: false }));
      });

      server.middlewares.use('/api/loop-ops/sync', (req, res) => {
        if (req.method !== 'POST') { res.statusCode = 405; res.end(); return; }
        if (blockNonLocal(req, res)) return;
        // No env option — the child inherits the dev server's environment.
        const child = spawn('node', [join(server.config.root, 'sync', 'loop-ops-import.mjs')], {
          cwd: server.config.root,
        });
        let stdout = '';
        let stderr = '';
        child.stdout.on('data', (c) => { stdout += c.toString(); });
        child.stderr.on('data', (c) => { stderr += c.toString(); });
        const timeout = setTimeout(() => child.kill(), 90_000);
        child.on('close', (code) => {
          clearTimeout(timeout);
          let mtime = null;
          try { mtime = statSync(join(server.config.root, 'public', 'data', 'loop-ops', 'spec.json')).mtimeMs; } catch { /* not written yet */ }
          res.setHeader('Content-Type', 'application/json');
          res.end(JSON.stringify({ ok: code === 0, code, stdout: stdout.slice(-2000), stderr: stderr.slice(-500), mtime }));
        });
        child.on('error', (err) => {
          clearTimeout(timeout);
          res.statusCode = 500;
          res.setHeader('Content-Type', 'application/json');
          res.end(JSON.stringify({ ok: false, error: err.message }));
        });
      });

      // Dev-mode mirror of sync/local-api.mjs for the hidden Seats page.
      server.middlewares.use('/api/superadmin-usage/status', (req, res) => {
        if (req.method !== 'GET') { res.statusCode = 405; res.end(); return; }
        if (blockNonLocal(req, res)) return;
        try {
          const filePath = join(server.config.root, 'public', 'data', 'superadmin-usage.json');
          const stat = statSync(filePath);
          res.setHeader('Content-Type', 'application/json');
          res.end(JSON.stringify({
            ok: true,
            mtime: stat.mtimeMs,
            size: stat.size,
            productionWritesEnabled: false,
          }));
        } catch {
          res.setHeader('Content-Type', 'application/json');
          res.end(JSON.stringify({ ok: false, productionWritesEnabled: false }));
        }
      });

      server.middlewares.use('/api/superadmin-usage/sync', (req, res) => {
        if (req.method !== 'POST') { res.statusCode = 405; res.end(); return; }
        if (blockNonLocal(req, res)) return;
        const child = spawn('node', [join(server.config.root, 'sync', 'superadmin-usage.mjs')], {
          cwd: server.config.root,
          env: process.env,
        });
        let stdout = '';
        let stderr = '';
        child.stdout.on('data', (c) => { stdout += c.toString(); });
        child.stderr.on('data', (c) => { stderr += c.toString(); });
        const timeout = setTimeout(() => child.kill(), 120_000);
        child.on('close', (code) => {
          clearTimeout(timeout);
          let mtime = null;
          try { mtime = statSync(join(server.config.root, 'public', 'data', 'superadmin-usage.json')).mtimeMs; } catch { /* not written yet */ }
          res.setHeader('Content-Type', 'application/json');
          res.end(JSON.stringify({ ok: code === 0, code, stdout: stdout.slice(-2000), stderr: stderr.slice(-500), mtime }));
        });
        child.on('error', (err) => {
          clearTimeout(timeout);
          res.statusCode = 500;
          res.setHeader('Content-Type', 'application/json');
          res.end(JSON.stringify({ ok: false, error: err.message }));
        });
      });

      server.middlewares.use('/api/sync', (req, res) => {
        if (req.method !== 'POST') {
          res.statusCode = 405;
          res.end();
          return;
        }
        if (blockNonLocal(req, res)) return;
        const started = startSyncRun({
          repoRoot: server.config.root,
          node: process.execPath,
          trigger: 'dashboard-dev',
          env: process.env,
        });
        res.statusCode = started.accepted ? 202 : 409;
        res.setHeader('Content-Type', 'application/json');
        res.end(JSON.stringify({
          ok: started.accepted,
          accepted: started.accepted,
          busy: started.busy,
          run_id: started.run_id,
          status: started.snapshot,
        }));
      });
    },
  };
}

export default defineConfig({
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
    },
    // dedupe: force a single copy of three / react / react-dom across the
    // dep tree. Without this, @react-three/postprocessing transitively pulls
    // stats-gl which ships its own three, causing "Multiple instances of
    // THREE" + "Invalid hook call" runtime errors that black-screen the
    // canvas. With dedupe, all three.js + react references resolve to the
    // root install, fixing bloom + future R3F-postprocessing usage.
    dedupe: ['three', 'react', 'react-dom'],
  },
  plugins: [react(), tailwindcss(), meowSyncPlugin(), publicDemoAssets(join(__dirname, 'public'))],
  build: {
    copyPublicDir: false,
    // Split only stable vendor groups identified by a local generateBundle
    // module-size pass. includeDependenciesRecursively is off so react /
    // react-dom stay in the app graph (pulling them into a vendor group
    // would create circular chunk init). Route surfaces are split in App.jsx.
    rolldownOptions: {
      output: {
        codeSplitting: {
          includeDependenciesRecursively: false,
          groups: [
            { name: 'vendor-three', test: /[\\/]node_modules[\\/]three[\\/]/, priority: 40 },
            { name: 'vendor-r3f', test: /[\\/]node_modules[\\/](?:@react-three|three-stdlib)[\\/]/, priority: 30 },
            { name: 'vendor-ag-grid', test: /[\\/]node_modules[\\/]ag-grid-/, priority: 30 },
            // recharts stays in the app graph. Splitting it into vendor-recharts
            // left createSelector undefined in the production chunk (Today and
            // Ledger threw "s is not a function" inside the legend selector).
            { name: 'vendor-xyflow', test: /[\\/]node_modules[\\/]@xyflow[\\/]/, priority: 30 },
            { name: 'vendor-motion', test: /[\\/]node_modules[\\/](?:framer-motion|motion-dom|motion-utils)[\\/]/, priority: 30 },
          ],
        },
      },
    },
  },
});