Spaces:
Running
Running
File size: 5,827 Bytes
b30b7c5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | // Vista «Arquitectura» — grafo 3D de dependencias del proyecto, FIEL a CodeFlow
// (braedonsaunders/codeflow, MIT): usa la MISMA librería `3d-force-graph`, con
// nodos coloreados por capa (carpeta de primer nivel), tamaño por número de
// conexiones y partículas de flujo viajando por los enlaces (la firma de
// CodeFlow). Lee los ficheros reales, extrae los imports y resuelve las aristas.
// Clic en un nodo → abre el fichero en Monaco.
import * as code from './tools/code.js';
const CODE_EXT = /\.(js|mjs|cjs|jsx|ts|tsx|py|go|rs|java|c|h|cpp|hpp|vue|svelte)$/i;
const FG_URL = 'https://cdn.jsdelivr.net/npm/3d-force-graph@1/dist/3d-force-graph.min.js';
// paleta por capa (carpeta de primer nivel), estilo CodeFlow
const LAYER_COLORS = ['#7c5cff', '#49e8ff', '#ff4d8d', '#3fb970', '#ffd479', '#ff9f45', '#a86cff', '#42b883', '#e34c26', '#00add8'];
// Extrae destinos de import de un fichero (JS/TS/Py, tolerante).
function importsOf(path, src) {
const t = [];
const re = /(?:import\s+[^'"]*from\s+|import\s+|require\(\s*|export\s+[^'"]*from\s+)['"]([^'"]+)['"]/g;
let m; while ((m = re.exec(src))) t.push(m[1]);
const py = /^\s*(?:from\s+([\w.]+)\s+import|import\s+([\w.]+))/gm;
while ((m = py.exec(src))) t.push((m[1] || m[2]).replace(/\./g, '/'));
return t;
}
// Resuelve un import relativo a un fichero real del proyecto.
function resolve(from, spec, files) {
if (!/^[./]/.test(spec)) {
const hit = files.find(f => f.endsWith('/' + spec) || f.includes('/' + spec + '.') || f.includes('/' + spec + '/'));
return hit || null;
}
const base = from.split('/').slice(0, -1);
for (const part of spec.split('/')) { if (part === '..') base.pop(); else if (part !== '.') base.push(part); }
const cand = base.join('/');
return files.find(f => f === cand) ||
files.find(f => f.replace(CODE_EXT, '') === cand) ||
files.find(f => f === cand + '/index.js' || f === cand + '/__init__.py') || null;
}
// Monaco carga un loader AMD (define.amd); el UMD de 3d-force-graph se
// registraría por AMD y NO crearía window.ForceGraph3D. Anulamos `define`
// mientras carga el bundle para forzar la rama global (igual que con xterm).
const loadFG = () => window.ForceGraph3D ? Promise.resolve() : new Promise((res, rej) => {
const savedDefine = window.define;
window.define = undefined;
const restore = () => { window.define = savedDefine; };
const s = document.createElement('script');
s.src = FG_URL;
s.onload = () => { restore(); window.ForceGraph3D ? res() : rej(new Error('3d-force-graph cargó pero no expuso ForceGraph3D')); };
s.onerror = () => { restore(); rej(new Error('no cargó 3d-force-graph')); };
document.head.appendChild(s);
});
let graph = null;
export async function renderArchitecture(container, onOpenFile) {
disposeArch();
container.innerHTML = '<div class="view-loading">Analizando la arquitectura del proyecto…</div>';
const files = (await code.fileList()).filter(f => CODE_EXT.test(f)).slice(0, 400);
if (!files.length) { container.innerHTML = '<div class="view-loading">Sin ficheros de código para analizar.</div>'; return; }
const topOf = p => p.includes('/') ? p.split('/')[0] : '(raíz)';
const layers = [...new Set(files.map(topOf))];
const layerColor = new Map(layers.map((l, i) => [l, LAYER_COLORS[i % LAYER_COLORS.length]]));
const deg = new Map(files.map(f => [f, 0]));
const links = [];
const seen = new Set();
for (const p of files) {
let src = ''; try { src = await code.read({ path: p }); } catch { continue; }
for (const spec of importsOf(p, src)) {
const target = resolve(p, spec, files);
if (target && target !== p) {
const key = p + ' |