vite.config.js
Rama main · réplica de lectura del repositorio original.
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 },
],
},
},
},
},
});