Spaces:
Running
Running
| // 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 + ' |