@wtdlee/repomap 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +61 -13
  2. package/dist/analyzers/index.d.ts +175 -25
  3. package/dist/analyzers/index.js +1 -1
  4. package/dist/{chunk-QZWPOG5B.js → chunk-GCIRJGW3.js} +78 -45
  5. package/dist/chunk-H7VVRHQZ.js +34 -0
  6. package/dist/chunk-HPBPEGHS.js +19 -0
  7. package/dist/{chunk-WQANJ7IA.js → chunk-JDM7Y7PX.js} +34 -28
  8. package/dist/{chunk-H4YGP3GL.js → chunk-OQAXO3X2.js} +346 -22
  9. package/dist/chunk-TNUKDIO7.js +5 -0
  10. package/dist/cli.js +21 -35
  11. package/dist/dataflow-analyzer-CJ2T0cGS.d.ts +345 -0
  12. package/dist/generators/assets/docs.css +176 -46
  13. package/dist/generators/assets/favicon/apple-touch-icon.png +0 -0
  14. package/dist/generators/assets/favicon/favicon-96x96.png +0 -0
  15. package/dist/generators/assets/favicon/favicon.ico +0 -0
  16. package/dist/generators/assets/favicon/favicon.svg +3 -0
  17. package/dist/generators/assets/favicon/site.webmanifest +21 -0
  18. package/dist/generators/assets/favicon/web-app-manifest-192x192.png +0 -0
  19. package/dist/generators/assets/favicon/web-app-manifest-512x512.png +0 -0
  20. package/dist/generators/assets/page-map.css +392 -87
  21. package/dist/generators/assets/rails-map.css +221 -48
  22. package/dist/generators/index.d.ts +0 -8
  23. package/dist/generators/index.js +1 -1
  24. package/dist/index.d.ts +18 -9
  25. package/dist/index.js +1 -1
  26. package/dist/page-map-generator-3GO6GL2P.js +1 -0
  27. package/dist/{rails-FFISZ4AE.js → rails-3HNUFTQV.js} +1 -1
  28. package/dist/rails-map-generator-CAQZUBI6.js +1 -0
  29. package/dist/server/index.d.ts +2 -6
  30. package/dist/server/index.js +1 -1
  31. package/dist/types.d.ts +12 -3
  32. package/package.json +1 -5
  33. package/dist/chunk-BPV4UZSW.js +0 -2
  34. package/dist/chunk-PTR5IROV.js +0 -36
  35. package/dist/chunk-XWZH2RDG.js +0 -19
  36. package/dist/dataflow-analyzer-s6ufFkKC.d.ts +0 -215
  37. package/dist/page-map-generator-HBKSOX2E.js +0 -1
  38. package/dist/rails-map-generator-UFLCMFAT.js +0 -1
@@ -1,19 +1,26 @@
1
- import {a as a$2}from'./chunk-H4YGP3GL.js';import {a as a$3}from'./chunk-VV3A3UE3.js';import {e,d as d$1,c,b as b$2}from'./chunk-BPV4UZSW.js';import {a,b as b$1}from'./chunk-XWZH2RDG.js';import {a as a$1}from'./chunk-QZWPOG5B.js';import {k}from'./chunk-PTR5IROV.js';import {simpleGit}from'simple-git';import*as d from'fs/promises';import*as m from'path';import H from'fast-glob';import*as w from'crypto';import F from'express';import {Server}from'socket.io';import*as z from'http';import {marked}from'marked';import*as P from'net';var C="1.1",x=class{cacheDir;manifest;manifestPath;dirty=false;constructor(a){this.cacheDir=m.join(a,".repomap-cache"),this.manifestPath=m.join(this.cacheDir,"manifest.json"),this.manifest={version:C,entries:{}};}async init(){try{await d.mkdir(this.cacheDir,{recursive:!0});}catch(a){console.warn(` Warning: Could not create cache directory: ${a.message}`);return}try{let a=await d.readFile(this.manifestPath,"utf-8"),t=JSON.parse(a);t.version===C?this.manifest=t:(console.log(" Cache version mismatch, clearing cache..."),await this.clear());}catch{}}async computeFileHash(a){try{let t=await d.readFile(a,"utf-8");return w.createHash("md5").update(t).digest("hex")}catch{return ""}}async computeFilesHash(a){let t=[...a].sort(),e=50,o;t.length<=e*2?o=t:o=[...t.slice(0,e),...t.slice(-e)];let s=await Promise.all(o.map(i=>this.computeFileHash(i))),n=w.createHash("md5").update(String(t.length)).digest("hex");return w.createHash("md5").update(s.join("")+n).digest("hex")}get(a,t){let e=this.manifest.entries[a];return e&&e.hash===t?e.data:null}set(a,t,e){this.manifest.entries[a]={hash:t,timestamp:Date.now(),data:e},this.dirty=true;}async save(){if(this.dirty)try{await d.mkdir(this.cacheDir,{recursive:!0}),await d.writeFile(this.manifestPath,JSON.stringify(this.manifest,null,2)),this.dirty=!1;}catch(a){console.warn(" Warning: Failed to save cache:",a.message);}}async clear(){this.manifest={version:C,entries:{}},this.dirty=true;try{await d.rm(this.cacheDir,{recursive:!0,force:!0}),await d.mkdir(this.cacheDir,{recursive:!0});}catch{}}getStats(){let a=Object.keys(this.manifest.entries).length,t=JSON.stringify(this.manifest).length;return {entries:a,size:t>1024*1024?`${(t/1024/1024).toFixed(1)}MB`:`${(t/1024).toFixed(1)}KB`}}};var b=class{config;mermaidGenerator;markdownGenerator;noCache;constructor(a$1,t){this.config=a$1,this.mermaidGenerator=new a,this.markdownGenerator=new b$1,this.noCache=t?.noCache??false;}async generate(){console.log(`\u{1F680} Starting documentation generation...
2
- `);let a=[];for(let i of this.config.repositories)try{console.log(`
3
- \u{1F4E6} Analyzing ${i.displayName}...`);let r=await this.analyzeRepository(i);a.push(r),console.log(`\u2705 Completed ${i.displayName}`);}catch(r){console.error(`\u274C Failed to analyze ${i.name}:`,r.message);}console.log(`
4
- \u{1F517} Running cross-repository analysis...`);let t=this.analyzeCrossRepo(a);console.log(`
5
- \u{1F4CA} Generating diagrams...`);let e=a.map(i=>i.analysis),o=this.extractCrossRepoLinks(e),s=this.mermaidGenerator.generateAll(e,o),n={generatedAt:new Date().toISOString(),repositories:a,crossRepoAnalysis:t,diagrams:s};return console.log(`
6
- \u{1F4DD} Writing documentation...`),await this.writeDocumentation(n),console.log(`
7
- \u2728 Documentation generation complete!`),console.log(`\u{1F4C1} Output: ${this.config.outputDir}`),n}async analyzeRepository(a){let t=new x(a.path);await t.init();let{version:e,commitHash:o}=await this.getRepoInfo(a),s=await H(["**/*.{ts,tsx,graphql}"],{cwd:a.path,ignore:["**/node_modules/**","**/.next/**","**/dist/**","**/build/**"],absolute:true}),n=await t.computeFilesHash(s),i=`analysis_v${e}_${a.name}_${o}`;this.noCache&&console.log(" \u{1F504} Cache disabled, analyzing from scratch...");let r=this.noCache?null:t.get(i,n);if(r){console.log(` \u26A1 Using cached analysis (hash: ${n.slice(0,8)}...)`);let c={totalPages:r.pages.length,totalComponents:r.components.length,totalGraphQLOperations:r.graphqlOperations.length,totalDataFlows:r.dataFlows.length,authRequiredPages:r.pages.filter(q=>q.authentication.required).length,publicPages:r.pages.filter(q=>!q.authentication.required).length};return {name:a.name,displayName:a.displayName,version:e,commitHash:o,analysis:r,summary:c}}let l=a.analyzers.map(c=>this.createAnalyzer(c,a)).filter(c=>c!==null);console.log(` Running ${l.length} analyzers in parallel...`);let h=Date.now(),g=await Promise.all(l.map(c=>c.analyze()));console.log(` Analysis completed in ${((Date.now()-h)/1e3).toFixed(1)}s`);let p=this.mergeAnalysisResults(g,a.name,e,o);t.set(i,n,p),await t.save(),console.log(` \u{1F4BE} Analysis cached (hash: ${n.slice(0,8)}...)`);let B={totalPages:p.pages.length,totalComponents:p.components.length,totalGraphQLOperations:p.graphqlOperations.length,totalDataFlows:p.dataFlows.length,authRequiredPages:p.pages.filter(c=>c.authentication.required).length,publicPages:p.pages.filter(c=>!c.authentication.required).length};return {name:a.name,displayName:a.displayName,version:e,commitHash:o,analysis:p,summary:B}}async getRepoInfo(a){try{let o=(await simpleGit(a.path).log({n:1})).latest?.hash||"unknown",s="unknown";try{let n=m.join(a.path,"package.json");s=JSON.parse(await d.readFile(n,"utf-8")).version||"unknown";}catch{}return {version:s,commitHash:o}}catch{return {version:"unknown",commitHash:"unknown"}}}createAnalyzer(a,t){switch(a){case "pages":if(t.type==="nextjs"||t.type==="rails"||t.type==="generic")return new b$2(t);break;case "graphql":return new c(t);case "dataflow":case "components":return new d$1(t);case "rest-api":case "api":return new e(t)}return null}mergeAnalysisResults(a,t,e,o){let s={repository:t,timestamp:new Date().toISOString(),version:e,commitHash:o,pages:[],graphqlOperations:[],apiCalls:[],components:[],dataFlows:[],apiEndpoints:[],models:[],crossRepoLinks:[]};for(let n of a)n.pages&&s.pages.push(...n.pages),n.graphqlOperations&&s.graphqlOperations.push(...n.graphqlOperations),n.apiCalls&&s.apiCalls.push(...n.apiCalls),n.components&&s.components.push(...n.components),n.dataFlows&&s.dataFlows.push(...n.dataFlows),n.apiEndpoints&&s.apiEndpoints.push(...n.apiEndpoints),n.models&&s.models.push(...n.models),n.crossRepoLinks&&s.crossRepoLinks.push(...n.crossRepoLinks);return s}analyzeCrossRepo(a){let t=[],e=[],o=[],s=[],n=new Map;for(let l of a)for(let h of l.analysis.graphqlOperations){let g=n.get(h.name)||[];g.push(l.name),n.set(h.name,g);}for(let[l,h]of n)h.length>1&&t.push(l);let i=a.filter(l=>l.analysis.pages.length>0),r=a.filter(l=>l.analysis.apiEndpoints.length>0);for(let l of i)for(let h of r)for(let g of h.analysis.apiEndpoints)e.push({frontend:l.name,backend:h.name,endpoint:g.path,operations:l.analysis.graphqlOperations.filter(p=>p.usedIn.length>0).map(p=>p.name)});return {sharedTypes:t,apiConnections:e,navigationFlows:o,dataFlowAcrossRepos:s}}extractCrossRepoLinks(a){let t=[],e=new Map;for(let o of a)for(let s of o.graphqlOperations){let n=e.get(s.name)||[];n.push(o),e.set(s.name,n);}for(let[o,s]of e)s.length>1&&t.push({sourceRepo:s[0].repository,sourcePath:`graphql/${o}`,targetRepo:s[1].repository,targetPath:`graphql/${o}`,linkType:"graphql-operation",description:`Shared GraphQL operation: ${o}`});return t}async writeDocumentation(a){let t=this.config.outputDir;await d.mkdir(t,{recursive:true});let e=this.markdownGenerator.generateDocumentation(a);for(let[s,n]of e){let i=m.join(t,s),r=m.dirname(i);await d.mkdir(r,{recursive:true}),await d.writeFile(i,n,"utf-8"),console.log(` \u{1F4C4} ${s}`);}let o=m.join(t,"report.json");await d.writeFile(o,JSON.stringify(a,null,2),"utf-8"),console.log(" \u{1F4CB} report.json");}};function Q(u){return new Promise(a=>{let t=P.createServer();t.once("error",e=>{e.code,a(false);}),t.once("listening",()=>{t.close(),a(true);}),t.listen(u);})}async function O(u,a=10){for(let t=0;t<a;t++){let e=u+t;if(await Q(e))return e}throw new Error(`No available port found between ${u} and ${u+a-1}`)}var I=class{config;port;app;server;io;engine;currentReport=null;envResult=null;railsAnalysis=null;constructor(a,t=3030,e){this.config=a,this.port=t,this.app=F(),this.server=z.createServer(this.app),this.io=new Server(this.server),this.engine=new b(a,{noCache:e?.noCache}),this.setupRoutes(),this.setupSocketIO();}setupRoutes(){this.app.use("/assets",F.static(m.join(this.config.outputDir,"assets"))),["common.css","page-map.css","docs.css","rails-map.css"].forEach(t=>{this.app.get(`/${t}`,async(e,o)=>{let s=[m.join(m.dirname(new URL(import.meta.url).pathname),"generators","assets",t),m.join(m.dirname(new URL(import.meta.url).pathname),"..","generators","assets",t),m.join(process.cwd(),"dist","generators","assets",t),m.join(process.cwd(),"src","generators","assets",t)];for(let n of s)try{let i=await d.readFile(n,"utf-8");o.type("text/css").send(i);return}catch{}o.status(404).send("CSS not found");});}),this.app.get("/",(t,e)=>{e.redirect("/page-map");}),this.app.get("/page-map",(t,e)=>{if(!this.currentReport){e.status(503).send("Documentation not ready yet");return}let o=new a$1;e.send(o.generatePageMapHtml(this.currentReport,{envResult:this.envResult,railsAnalysis:this.railsAnalysis}));}),this.app.get("/rails-map",(t,e)=>{if(!this.railsAnalysis){e.status(404).send("No Rails environment detected");return}let o=new a$2;e.send(o.generateFromResult(this.railsAnalysis));}),this.app.get("/docs",async(t,e)=>{e.send(await this.renderPage("index"));}),this.app.get("/docs/*path",async(t,e)=>{let o=t.params.path,s=Array.isArray(o)?o.join("/"):o||"index";e.send(await this.renderPage(s));}),this.app.get("/api/report",(t,e)=>{e.json(this.currentReport);}),this.app.get("/api/env",(t,e)=>{e.json(this.envResult);}),this.app.get("/api/rails",(t,e)=>{this.railsAnalysis?e.json(this.railsAnalysis):e.status(404).json({error:"No Rails analysis available"});}),this.app.get("/api/diagram/:name",(t,e)=>{let o=this.currentReport?.diagrams.find(s=>s.title.toLowerCase().replace(/\s+/g,"-")===t.params.name);o?e.json(o):e.status(404).json({error:"Diagram not found"});}),this.app.post("/api/regenerate",async(t,e)=>{try{await this.regenerate(),e.json({success:!0});}catch(o){e.status(500).json({error:o.message});}});}setupSocketIO(){this.io.on("connection",a=>{console.log("Client connected"),a.on("disconnect",()=>{console.log("Client disconnected");});});}async renderPage(a){let t=a.replace(/\.md$/,""),e=m.join(this.config.outputDir,`${t}.md`),o="";try{let s=await d.readFile(e,"utf-8"),n=await marked.parse(s);n=n.replace(/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g,'<div class="mermaid">$1</div>'),n=n.replace(/<table>/g,'<div class="table-wrapper"><table>'),n=n.replace(/<\/table>/g,"</table></div>"),o=n;}catch(s){console.error(`Failed to render page: ${e}`,s),o=`<h1>Page not found</h1><p>Path: ${t}</p>`;}return this.getHtmlTemplate(o)}getGraphQLData(){if(!this.currentReport)return "[]";let a=[];for(let t of this.currentReport.repositories)for(let e of t.analysis?.graphqlOperations||[])a.push({name:e.name,type:e.type,returnType:e.returnType,variables:e.variables,fields:e.fields,usedIn:e.usedIn});return JSON.stringify(a)}getApiCallsData(){if(!this.currentReport)return "[]";let a=[];for(let t of this.currentReport.repositories)for(let e of t.analysis?.apiCalls||[])a.push({id:e.id,method:e.method,url:e.url,callType:e.callType,filePath:e.filePath,line:e.line,containingFunction:e.containingFunction,requiresAuth:e.requiresAuth});return JSON.stringify(a)}getHtmlTemplate(a){let t=this.getGraphQLData(),e=this.getApiCallsData();return `<!DOCTYPE html>
8
- <html lang="ja">
1
+ import {a as a$2}from'./chunk-OQAXO3X2.js';import {a as a$3}from'./chunk-VV3A3UE3.js';import {B,A as A$1,z,y}from'./chunk-TNUKDIO7.js';import {a,b}from'./chunk-HPBPEGHS.js';import {a as a$1}from'./chunk-GCIRJGW3.js';import {k}from'./chunk-H7VVRHQZ.js';import {simpleGit}from'simple-git';import*as u from'fs/promises';import*as d from'path';import E from'express';import {Server}from'socket.io';import*as O from'http';import {marked}from'marked';import*as C from'net';var v=class{config;mermaidGenerator;markdownGenerator;constructor(a$1){this.config=a$1,this.mermaidGenerator=new a,this.markdownGenerator=new b;}async generate(){let a=[];for(let r of this.config.repositories)try{let i=await this.analyzeRepository(r);a.push(i);}catch(i){console.error(`\u274C ${r.name}: ${i.message}`);}let s=this.analyzeCrossRepo(a),e=a.map(r=>r.analysis),n=this.extractCrossRepoLinks(e),t=this.mermaidGenerator.generateAll(e,n),o={generatedAt:new Date().toISOString(),repositories:a,crossRepoAnalysis:s,diagrams:t};return await this.writeDocumentation(o),o}async analyzeRepository(a){let{version:s,commitHash:e}=await this.getRepoInfo(a),n=a.analyzers.map(p=>this.createAnalyzer(p,a)).filter(p=>p!==null),t=Date.now(),o=await Promise.all(n.map(p=>p.analyze())),r=((Date.now()-t)/1e3).toFixed(1);console.log(` Analyzed ${a.displayName} in ${r}s`);let i=this.mergeAnalysisResults(o,a.name,s,e);this.enrichPagesWithHookGraphQL(i);let l={totalPages:i.pages.length,totalComponents:i.components.length,totalGraphQLOperations:i.graphqlOperations.length,totalDataFlows:i.dataFlows.length,authRequiredPages:i.pages.filter(p=>p.authentication.required).length,publicPages:i.pages.filter(p=>!p.authentication.required).length};return {name:a.name,displayName:a.displayName,version:s,commitHash:e,analysis:i,summary:l}}async getRepoInfo(a){try{let n=(await simpleGit(a.path).log({n:1})).latest?.hash||"unknown",t="unknown";try{let o=d.join(a.path,"package.json");t=JSON.parse(await u.readFile(o,"utf-8")).version||"unknown";}catch{}return {version:t,commitHash:n}}catch{return {version:"unknown",commitHash:"unknown"}}}createAnalyzer(a,s){switch(a){case "pages":if(s.type==="nextjs"||s.type==="rails"||s.type==="generic")return new y(s);break;case "graphql":return new z(s);case "dataflow":case "components":return new A$1(s);case "rest-api":case "api":return new B(s)}return null}mergeAnalysisResults(a,s,e,n){let t={repository:s,timestamp:new Date().toISOString(),version:e,commitHash:n,pages:[],graphqlOperations:[],apiCalls:[],components:[],dataFlows:[],apiEndpoints:[],models:[],crossRepoLinks:[]};for(let o of a)o.pages&&t.pages.push(...o.pages),o.graphqlOperations&&t.graphqlOperations.push(...o.graphqlOperations),o.apiCalls&&t.apiCalls.push(...o.apiCalls),o.components&&t.components.push(...o.components),o.dataFlows&&t.dataFlows.push(...o.dataFlows),o.apiEndpoints&&t.apiEndpoints.push(...o.apiEndpoints),o.models&&t.models.push(...o.models),o.crossRepoLinks&&t.crossRepoLinks.push(...o.crossRepoLinks);return t}analyzeCrossRepo(a){let s=[],e=[],n=[],t=[],o=new Map;for(let l of a)for(let p of l.analysis.graphqlOperations){let c=o.get(p.name)||[];c.push(l.name),o.set(p.name,c);}for(let[l,p]of o)p.length>1&&s.push(l);let r=a.filter(l=>l.analysis.pages.length>0),i=a.filter(l=>l.analysis.apiEndpoints.length>0);for(let l of r)for(let p of i)for(let c of p.analysis.apiEndpoints)e.push({frontend:l.name,backend:p.name,endpoint:c.path,operations:l.analysis.graphqlOperations.filter(m=>m.usedIn.length>0).map(m=>m.name)});return {sharedTypes:s,apiConnections:e,navigationFlows:n,dataFlowAcrossRepos:t}}extractCrossRepoLinks(a){let s=[],e=new Map;for(let n of a)for(let t of n.graphqlOperations){let o=e.get(t.name)||[];o.push(n),e.set(t.name,o);}for(let[n,t]of e)t.length>1&&s.push({sourceRepo:t[0].repository,sourcePath:`graphql/${n}`,targetRepo:t[1].repository,targetPath:`graphql/${n}`,linkType:"graphql-operation",description:`Shared GraphQL operation: ${n}`});return s}enrichPagesWithHookGraphQL(a){let s=new Map;for(let t of a.graphqlOperations){if(!t.filePath)continue;let r=(t.filePath.split("/").pop()||"").replace(/\.(ts|tsx|js|jsx)$/,"");r.startsWith("use")&&(s.has(r)||s.set(r,new Set),s.get(r).add(t.name));}for(let t of a.components){if(t.type!=="hook")continue;let o=[];for(let r of t.hooks){let i=r.match(/^(Query|Mutation|Subscription):\s*(.+)$/);i&&o.push(i[2]);}if(o.length>0){s.has(t.name)||s.set(t.name,new Set);for(let r of o)s.get(t.name).add(r);}}let e=new Map;for(let t of a.graphqlOperations){if(t.type!=="query"&&t.type!=="mutation"&&t.type!=="subscription"||!t.filePath)continue;let o=t.filePath.replace(/\.(ts|tsx|js|jsx)$/,"");e.has(o)||e.set(o,[]),e.get(o).push({opName:t.name,opType:t.type});}let n=new Map;for(let t of a.graphqlOperations)(t.type==="query"||t.type==="mutation"||t.type==="subscription")&&n.set(t.name,t.type);for(let t of a.pages){let o=new Set(t.dataFetching.map(l=>l.operationName?.replace(/^[→\->\s]+/,"")||"")),r=a.components.find(l=>l.filePath===`src/pages/${t.filePath}`);if(!r)continue;let i=[];i.push(...r.hooks.filter(l=>l.startsWith("use")));for(let l of r.dependencies)l.startsWith("use")&&i.push(l);for(let l of i){let p=s.get(l);if(p)for(let c of p){if(o.has(c))continue;o.add(c);let m=n.get(c)||"query";t.dataFetching.push({type:m==="mutation"?"useMutation":"useQuery",operationName:c,source:`hook:${l}`});}}if(r.imports)for(let l of r.imports){let p=d.dirname(r.filePath),c=l.path;l.path.startsWith(".")?(c=d.join(p,l.path),c=d.normalize(c)):l.path.startsWith("@/")&&(c=l.path.replace("@/","src/")),c=c.replace(/\.(ts|tsx|js|jsx)$/,"");let m=e.get(c);if(m)for(let y of m)o.has(y.opName)||(o.add(y.opName),t.dataFetching.push({type:y.opType==="mutation"?"useMutation":"useQuery",operationName:y.opName,source:`import:${l.path}`}));}}for(let t of a.components){if(t.type!=="container"&&t.type!=="page")continue;let o=a.pages.find(i=>i.component===t.name||i.filePath?.includes(t.name));if(!o)continue;let r=new Set(o.dataFetching.map(i=>i.operationName?.replace(/^[→\->\s]+/,"")||""));for(let i of t.hooks){if(!i.startsWith("use"))continue;let l=s.get(i);if(l)for(let p of l){if(r.has(p))continue;r.add(p);let c=n.get(p)||"query";o.dataFetching.push({type:c==="mutation"?"useMutation":"useQuery",operationName:p,source:`component:${t.name}`});}}for(let i of t.dependencies){if(!i.startsWith("use"))continue;let l=s.get(i);if(l)for(let p of l){if(r.has(p))continue;r.add(p);let c=n.get(p)||"query";o.dataFetching.push({type:c==="mutation"?"useMutation":"useQuery",operationName:p,source:`component:${t.name}`});}}if(t.imports)for(let i of t.imports){let l=d.dirname(t.filePath),p=i.path;i.path.startsWith(".")?p=d.normalize(d.join(l,i.path)):i.path.startsWith("@/")&&(p=i.path.replace("@/","src/")),p=p.replace(/\.(ts|tsx|js|jsx)$/,"");let c=e.get(p);if(c)for(let m of c)r.has(m.opName)||(r.add(m.opName),o.dataFetching.push({type:m.opType==="mutation"?"useMutation":"useQuery",operationName:m.opName,source:`component:${t.name}`}));}}}async writeDocumentation(a){let s=this.config.outputDir;await u.mkdir(s,{recursive:true});let e=this.markdownGenerator.generateDocumentation(a);for(let[t,o]of e){let r=d.join(s,t),i=d.dirname(r);await u.mkdir(i,{recursive:true}),await u.writeFile(r,o,"utf-8");}let n=d.join(s,"report.json");await u.writeFile(n,JSON.stringify(a,null,2),"utf-8");}};function A(f){return new Promise(a=>{let s=C.createServer();s.once("error",e=>{e.code,a(false);}),s.once("listening",()=>{s.close(),a(true);}),s.listen(f);})}async function L(f,a=10){for(let s=0;s<a;s++){let e=f+s;if(await A(e))return e}throw new Error(`No available port found between ${f} and ${f+a-1}`)}var P=class{config;port;app;server;io;engine;currentReport=null;envResult=null;railsAnalysis=null;constructor(a,s=3030){this.config=a,this.port=s,this.app=E(),this.server=O.createServer(this.app),this.io=new Server(this.server),this.engine=new v(a),this.setupRoutes(),this.setupSocketIO();}setupRoutes(){this.app.use("/assets",E.static(d.join(this.config.outputDir,"assets"))),["common.css","page-map.css","docs.css","rails-map.css"].forEach(e=>{this.app.get(`/${e}`,async(n,t)=>{let o=[d.join(d.dirname(new URL(import.meta.url).pathname),"generators","assets",e),d.join(d.dirname(new URL(import.meta.url).pathname),"..","generators","assets",e),d.join(process.cwd(),"dist","generators","assets",e),d.join(process.cwd(),"src","generators","assets",e)];for(let r of o)try{let i=await u.readFile(r,"utf-8");t.type("text/css").send(i);return}catch{}t.status(404).send("CSS not found");});}),["favicon.ico","favicon.svg","favicon-96x96.png","apple-touch-icon.png","site.webmanifest","web-app-manifest-192x192.png","web-app-manifest-512x512.png"].forEach(e=>{(e==="favicon.ico"?[`/${e}`,`/favicon/${e}`]:[`/favicon/${e}`]).forEach(t=>{this.app.get(t,async(o,r)=>{let i=[d.join(d.dirname(new URL(import.meta.url).pathname),"generators","assets","favicon",e),d.join(d.dirname(new URL(import.meta.url).pathname),"..","generators","assets","favicon",e),d.join(process.cwd(),"dist","generators","assets","favicon",e),d.join(process.cwd(),"src","generators","assets","favicon",e)];for(let l of i)try{let p=await u.readFile(l),c=e.split(".").pop(),m={ico:"image/x-icon",svg:"image/svg+xml",png:"image/png",webmanifest:"application/manifest+json"};r.type(m[c||""]||"application/octet-stream").send(p);return}catch{}r.status(404).send("File not found");});});}),this.app.get("/",(e,n)=>{n.redirect("/page-map");}),this.app.get("/page-map",(e,n)=>{if(!this.currentReport){n.status(503).send("Documentation not ready yet");return}let t=new a$1;n.send(t.generatePageMapHtml(this.currentReport,{envResult:this.envResult,railsAnalysis:this.railsAnalysis}));}),this.app.get("/rails-map",(e,n)=>{if(!this.railsAnalysis){n.status(404).send("No Rails environment detected");return}let t=new a$2;n.send(t.generateFromResult(this.railsAnalysis));}),this.app.get("/docs",async(e,n)=>{n.send(await this.renderPage("index"));}),this.app.get("/docs/*path",async(e,n)=>{let t=e.params.path,o=Array.isArray(t)?t.join("/"):t||"index";n.send(await this.renderPage(o));}),this.app.get("/api/report",(e,n)=>{n.json(this.currentReport);}),this.app.get("/api/env",(e,n)=>{n.json(this.envResult);}),this.app.get("/api/rails",(e,n)=>{this.railsAnalysis?n.json(this.railsAnalysis):n.status(404).json({error:"No Rails analysis available"});}),this.app.get("/api/diagram/:name",(e,n)=>{let t=this.currentReport?.diagrams.find(o=>o.title.toLowerCase().replace(/\s+/g,"-")===e.params.name);t?n.json(t):n.status(404).json({error:"Diagram not found"});}),this.app.post("/api/regenerate",async(e,n)=>{try{await this.regenerate(),n.json({success:!0});}catch(t){n.status(500).json({error:t.message});}});}setupSocketIO(){this.io.on("connection",a=>{a.on("disconnect",()=>{});});}async renderPage(a){let s=a.replace(/\.md$/,""),e=d.join(this.config.outputDir,`${s}.md`),n="";try{let t=await u.readFile(e,"utf-8"),o=await marked.parse(t);o=o.replace(/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g,'<div class="mermaid">$1</div>'),o=o.replace(/<table>/g,'<div class="table-wrapper"><table>'),o=o.replace(/<\/table>/g,"</table></div>"),n=o;}catch(t){let o=t;if(o.code==="ENOENT"){let r=this.currentReport?.repositories.map(i=>i.name)||[];n=`
2
+ <h1>Page not found</h1>
3
+ <p>The requested path <code>${s}</code> does not exist.</p>
4
+ ${r.length>0?`
5
+ <p>Available repositories:</p>
6
+ <ul>${r.map(i=>`<li><a href="/docs/repos/${i}">${i}</a></li>`).join("")}</ul>
7
+ `:""}
8
+ <p><a href="/">\u2190 Back to home</a></p>
9
+ `;}else console.error(`\u26A0\uFE0F Error reading ${e}: ${o.message}`),n=`<h1>Error</h1><p>Failed to load page: ${o.message}</p>`;}return this.getHtmlTemplate(n)}getGraphQLData(){if(!this.currentReport)return "[]";let a=[];for(let s of this.currentReport.repositories)for(let e of s.analysis?.graphqlOperations||[])a.push({name:e.name,type:e.type,returnType:e.returnType,variables:e.variables,fields:e.fields,usedIn:e.usedIn});return JSON.stringify(a)}getApiCallsData(){if(!this.currentReport)return "[]";let a=[];for(let s of this.currentReport.repositories)for(let e of s.analysis?.apiCalls||[])a.push({id:e.id,method:e.method,url:e.url,callType:e.callType,filePath:e.filePath,line:e.line,containingFunction:e.containingFunction,requiresAuth:e.requiresAuth});return JSON.stringify(a)}getHtmlTemplate(a){let s=this.getGraphQLData(),e=this.getApiCallsData();return `<!DOCTYPE html>
10
+ <html lang="en">
9
11
  <head>
10
12
  <meta charset="UTF-8">
11
13
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
12
14
  <title>${this.config.site.title}</title>
15
+ <link rel="icon" type="image/x-icon" href="/favicon.ico">
16
+ <link rel="icon" type="image/svg+xml" href="/favicon/favicon.svg">
17
+ <link rel="icon" type="image/png" sizes="96x96" href="/favicon/favicon-96x96.png">
18
+ <link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png">
19
+ <link rel="manifest" href="/favicon/site.webmanifest">
13
20
  <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
14
21
  <script src="/socket.io/socket.io.js"></script>
15
22
  <script>
16
- window.graphqlOps = ${t};
23
+ window.graphqlOps = ${s};
17
24
  window.apiCalls = ${e};
18
25
  // Create multiple lookup maps for different naming conventions
19
26
  window.gqlMap = new Map();
@@ -97,18 +104,18 @@ import {a as a$2}from'./chunk-H4YGP3GL.js';import {a as a$3}from'./chunk-VV3A3UE
97
104
  <div class="nav-group">
98
105
  <span class="nav-group-title">Documentation</span>
99
106
  <div class="nav-subitems">
100
- ${this.config.repositories.map(o=>`
101
- <a href="/docs/repos/${o.name}/pages">Pages</a>
102
- <a href="/docs/repos/${o.name}/components">Components</a>
103
- <a href="/docs/repos/${o.name}/graphql">GraphQL</a>
104
- <a href="/docs/repos/${o.name}/dataflow">Data Flow</a>
107
+ ${this.config.repositories.map(n=>`
108
+ <a href="/docs/repos/${n.name}/pages">Pages</a>
109
+ <a href="/docs/repos/${n.name}/components">Components</a>
110
+ <a href="/docs/repos/${n.name}/graphql">GraphQL</a>
111
+ <a href="/docs/repos/${n.name}/dataflow">Data Flow</a>
105
112
  `).join("")}
106
113
  </div>
107
114
  </div>
108
115
  <div class="nav-group">
109
116
  <span class="nav-group-title">Analysis</span>
110
117
  <div class="nav-subitems">
111
- <a href="/docs/cross-repo">Cross Repository</a>
118
+ ${this.config.repositories.length>1?'<a href="/docs/cross-repo">Cross Repository</a>':""}
112
119
  <a href="/docs/diagrams">Diagrams</a>
113
120
  </div>
114
121
  </div>
@@ -157,10 +164,10 @@ import {a as a$2}from'./chunk-H4YGP3GL.js';import {a as a$3}from'./chunk-VV3A3UE
157
164
  container.className = 'mermaid-container';
158
165
  container.innerHTML = \`
159
166
  <div class="mermaid-controls">
160
- <button onclick="zoomDiagram(\${idx}, 0.8)" title="\u7E2E\u5C0F">\u2796</button>
161
- <button onclick="zoomDiagram(\${idx}, 1.25)" title="\u62E1\u5927">\u2795</button>
162
- <button onclick="zoomDiagram(\${idx}, 'reset')" title="\u30EA\u30BB\u30C3\u30C8">\u{1F504}</button>
163
- <button onclick="toggleFullscreen(\${idx})" title="\u5168\u753B\u9762">\u26F6</button>
167
+ <button onclick="zoomDiagram(\${idx}, 0.8)" title="Zoom Out">\u2796</button>
168
+ <button onclick="zoomDiagram(\${idx}, 1.25)" title="Zoom In">\u2795</button>
169
+ <button onclick="zoomDiagram(\${idx}, 'reset')" title="Reset">\u{1F504}</button>
170
+ <button onclick="toggleFullscreen(\${idx})" title="Fullscreen">\u26F6</button>
164
171
  </div>
165
172
  <div class="mermaid-wrapper" id="wrapper-\${idx}">
166
173
  <div class="mermaid-inner" id="inner-\${idx}"></div>
@@ -925,7 +932,7 @@ import {a as a$2}from'./chunk-H4YGP3GL.js';import {a as a$3}from'./chunk-VV3A3UE
925
932
  async function regenerate() {
926
933
  try {
927
934
  const btn = document.querySelector('.regenerate-btn');
928
- btn.textContent = '\u23F3 \u751F\u6210\u4E2D...';
935
+ btn.textContent = '\u23F3 Generating...';
929
936
  btn.disabled = true;
930
937
 
931
938
  const res = await fetch('/api/regenerate', { method: 'POST' });
@@ -934,24 +941,23 @@ import {a as a$2}from'./chunk-H4YGP3GL.js';import {a as a$3}from'./chunk-VV3A3UE
934
941
  if (data.success) {
935
942
  window.location.reload();
936
943
  } else {
937
- alert('\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ' + data.error);
944
+ alert('Generation failed: ' + data.error);
938
945
  }
939
946
  } catch (e) {
940
- alert('\u30A8\u30E9\u30FC: ' + e.message);
947
+ alert('Error: ' + e.message);
941
948
  } finally {
942
949
  const btn = document.querySelector('.regenerate-btn');
943
- btn.textContent = '\u{1F504} \u518D\u751F\u6210';
950
+ btn.textContent = '\u{1F504} Regenerate';
944
951
  btn.disabled = false;
945
952
  }
946
953
  }
947
954
  </script>
948
955
  </body>
949
- </html>`}async start(a=true){let t=this.config.repositories[0]?.path||process.cwd();if(console.log("\u{1F50D} Detecting project environments..."),this.envResult=await a$3(t),this.envResult.environments.length>0){console.log(` Found: ${this.envResult.environments.map(e=>e.type).join(", ")}`);for(let e of this.envResult.environments)e.features.length>0&&console.log(` ${e.type} features: ${e.features.join(", ")}`);}if(console.log(`
950
- \u{1F4DA} Generating documentation...`),this.currentReport=await this.engine.generate(),this.envResult.hasRails){console.log(`
951
- \u{1F6E4}\uFE0F Analyzing Rails application...`);try{this.railsAnalysis=await k(t),console.log(" \u2705 Rails analysis complete");}catch(e){console.error(" \u26A0\uFE0F Rails analysis failed:",e.message);}}try{let e=await O(this.port);e!==this.port&&console.log(`
956
+ </html>`}async start(a=true){let s=this.config.repositories[0]?.path||process.cwd();this.envResult=await a$3(s),this.currentReport=await this.engine.generate(),console.log();for(let e of this.currentReport.repositories){let n=e.summary;console.log(` \u2705 ${e.displayName}`),console.log(` ${n.totalPages} pages \xB7 ${n.totalComponents} components \xB7 ${n.totalGraphQLOperations} GraphQL \xB7 ${n.totalDataFlows} data flows`);}if(this.envResult.hasRails){console.log(`
957
+ Rails...`);try{this.railsAnalysis=await k(s);let e=this.railsAnalysis.summary;console.log(` ${e.totalRoutes} routes \xB7 ${e.totalControllers} controllers \xB7 ${e.totalModels} models \xB7 ${e.totalGrpcServices} gRPC`);}catch(e){console.error(" \u26A0\uFE0F Rails analysis failed:",e.message);}}try{let e=await L(this.port);e!==this.port&&console.log(`
952
958
  \u26A0\uFE0F Port ${this.port} is in use, using port ${e} instead`),this.port=e;}catch(e){console.error(`
953
959
  \u274C Failed to find available port: ${e.message}`),process.exit(1);}if(this.server.listen(this.port,()=>{console.log(`
954
960
  \u{1F310} Documentation server running at http://localhost:${this.port}`),this.envResult?.hasRails&&this.envResult?.hasNextjs&&console.log(" \u{1F4CA} Multiple environments detected - use tabs to switch views"),console.log(` Press Ctrl+C to stop
955
961
  `);}),a){let e=(await import('open')).default;await e(`http://localhost:${this.port}`);}this.config.watch.enabled&&this.watchForChanges();}async regenerate(){if(console.log(`
956
- \u{1F504} Regenerating documentation...`),this.currentReport=await this.engine.generate(),this.envResult?.hasRails){let a=this.config.repositories[0]?.path||process.cwd();try{this.railsAnalysis=await k(a);}catch(t){console.error("\u26A0\uFE0F Rails re-analysis failed:",t.message);}}this.io.emit("reload"),console.log("\u2705 Documentation regenerated");}async watchForChanges(){let a=this.config.repositories.map(e=>e.path),t=null;for(let e of a)try{let o=d.watch(e,{recursive:!0});(async()=>{for await(let s of o)s.filename&&(s.filename.endsWith(".ts")||s.filename.endsWith(".tsx"))&&(t&&clearTimeout(t),t=setTimeout(async()=>{await this.regenerate();},this.config.watch.debounce));})();}catch(o){console.warn(`Warning: Could not watch directory ${e}:`,o.message);}}stop(){this.server.close(),console.log(`
957
- \u{1F44B} Server stopped`);}};export{b as a,I as b};
962
+ \u{1F504} Regenerating...`),this.currentReport=await this.engine.generate(),this.envResult?.hasRails){let a=this.config.repositories[0]?.path||process.cwd();try{this.railsAnalysis=await k(a);}catch(s){console.error("\u26A0\uFE0F Rails re-analysis failed:",s.message);}}for(let a of this.currentReport.repositories)console.log(` ${a.displayName}: ${a.summary.totalPages} pages \xB7 ${a.summary.totalComponents} components \xB7 ${a.summary.totalGraphQLOperations} GraphQL`);this.io.emit("reload"),console.log("\u2705 Done");}async watchForChanges(){let a=this.config.repositories.map(e=>e.path),s=null;for(let e of a)try{let n=u.watch(e,{recursive:!0});(async()=>{for await(let t of n)t.filename&&(t.filename.endsWith(".ts")||t.filename.endsWith(".tsx"))&&(s&&clearTimeout(s),s=setTimeout(async()=>{await this.regenerate();},this.config.watch.debounce));})();}catch(n){console.warn(`Warning: Could not watch directory ${e}:`,n.message);}}stop(){this.server.close(),console.log(`
963
+ \u{1F44B} Server stopped`);}};export{v as a,P as b};
@@ -1,10 +1,15 @@
1
- import {k}from'./chunk-PTR5IROV.js';import*as r from'fs';import*as c from'path';var o=class{constructor(t){this.rootPath=t;}result=null;async generate(t={}){if(!this.rootPath)throw new Error("Root path required for analysis");let{title:e="Rails Application Map"}=t;this.result=await k(this.rootPath);let a=this.generateHTML(e);return t.outputPath&&(r.writeFileSync(t.outputPath,a),console.log(`
2
- \u{1F4C4} Generated: ${t.outputPath}`)),a}generateFromResult(t,e="Rails Application Map"){return this.result=t,this.generateHTML(e)}generateHTML(t){if(!this.result)throw new Error("Analysis not run");let{routes:e,controllers:a,models:s,grpc:i,summary:l}=this.result;return `<!DOCTYPE html>
1
+ import {k}from'./chunk-H7VVRHQZ.js';import*as d from'fs';import*as c from'path';var o=class{constructor(t){this.rootPath=t;}result=null;async generate(t={}){if(!this.rootPath)throw new Error("Root path required for analysis");let{title:e="Rails Application Map"}=t;this.result=await k(this.rootPath);let s=this.generateHTML(e);return t.outputPath&&(d.writeFileSync(t.outputPath,s),console.log(`
2
+ \u{1F4C4} Generated: ${t.outputPath}`)),s}generateFromResult(t,e="Rails Application Map"){return this.result=t,this.generateHTML(e)}generateHTML(t){if(!this.result)throw new Error("Analysis not run");let{routes:e,controllers:s,models:a,grpc:l,summary:i}=this.result;return `<!DOCTYPE html>
3
3
  <html lang="en">
4
4
  <head>
5
5
  <meta charset="UTF-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>${t}</title>
7
+ <title>${t} - Repomap</title>
8
+ <link rel="icon" type="image/x-icon" href="/favicon.ico">
9
+ <link rel="icon" type="image/svg+xml" href="/favicon/favicon.svg">
10
+ <link rel="icon" type="image/png" sizes="96x96" href="/favicon/favicon-96x96.png">
11
+ <link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png">
12
+ <link rel="manifest" href="/favicon/site.webmanifest">
8
13
  <link rel="stylesheet" href="/rails-map.css">
9
14
  </head>
10
15
  <body>
@@ -18,25 +23,25 @@ import {k}from'./chunk-PTR5IROV.js';import*as r from'fs';import*as c from'path';
18
23
  <div class="stats-bar">
19
24
  <div class="stat active" data-view="routes">
20
25
  <div>
21
- <div class="stat-value">${l.totalRoutes.toLocaleString()}</div>
26
+ <div class="stat-value">${i.totalRoutes.toLocaleString()}</div>
22
27
  <div class="stat-label">Routes</div>
23
28
  </div>
24
29
  </div>
25
30
  <div class="stat" data-view="controllers">
26
31
  <div>
27
- <div class="stat-value">${l.totalControllers}</div>
32
+ <div class="stat-value">${i.totalControllers}</div>
28
33
  <div class="stat-label">Controllers</div>
29
34
  </div>
30
35
  </div>
31
36
  <div class="stat" data-view="models">
32
37
  <div>
33
- <div class="stat-value">${l.totalModels}</div>
38
+ <div class="stat-value">${i.totalModels}</div>
34
39
  <div class="stat-label">Models</div>
35
40
  </div>
36
41
  </div>
37
42
  <div class="stat" data-view="grpc">
38
43
  <div>
39
- <div class="stat-value">${l.totalGrpcServices}</div>
44
+ <div class="stat-value">${i.totalGrpcServices}</div>
40
45
  <div class="stat-label">gRPC</div>
41
46
  </div>
42
47
  </div>
@@ -57,7 +62,7 @@ import {k}from'./chunk-PTR5IROV.js';import*as r from'fs';import*as c from'path';
57
62
  </div>
58
63
 
59
64
  <div class="sidebar-section namespaces" id="namespaceFilter">
60
- <div class="sidebar-title">Namespaces (${l.namespaces.length})</div>
65
+ <div class="sidebar-title">Namespaces (${i.namespaces.length})</div>
61
66
  <div class="namespace-list">
62
67
  <div class="namespace-item active" data-namespace="all">
63
68
  <span>All</span>
@@ -90,9 +95,9 @@ import {k}from'./chunk-PTR5IROV.js';import*as r from'fs';import*as c from'path';
90
95
  <script>
91
96
  // Data
92
97
  const routes = ${JSON.stringify(e.routes)};
93
- const controllers = ${JSON.stringify(a.controllers)};
94
- const models = ${JSON.stringify(s.models)};
95
- const grpcServices = ${JSON.stringify(i.services)};
98
+ const controllers = ${JSON.stringify(s.controllers)};
99
+ const models = ${JSON.stringify(a.models)};
100
+ const grpcServices = ${JSON.stringify(l.services)};
96
101
 
97
102
  // State
98
103
  let currentView = 'routes';
@@ -575,6 +580,7 @@ import {k}from'./chunk-PTR5IROV.js';import*as r from'fs';import*as c from'path';
575
580
  // Re-render mermaid diagram
576
581
  container.removeAttribute('data-processed');
577
582
  window.mermaid.init(undefined, container);
583
+ setTimeout(initDiagramPanZoom, 100);
578
584
  } catch (e) {
579
585
  console.error('Mermaid error:', e);
580
586
  }
@@ -591,11 +597,304 @@ import {k}from'./chunk-PTR5IROV.js';import*as r from'fs';import*as c from'path';
591
597
  const diagram = document.getElementById('mermaid-diagram');
592
598
  if (diagram) {
593
599
  window.mermaid.init(undefined, diagram);
600
+ setTimeout(initDiagramPanZoom, 100);
594
601
  }
595
602
  };
596
603
  document.head.appendChild(script);
597
604
  }
598
605
 
606
+ // Pan and zoom functionality for mermaid diagram
607
+ function initDiagramPanZoom() {
608
+ const container = document.getElementById('mermaid-container');
609
+ const svg = container?.querySelector('svg');
610
+ if (!svg) return;
611
+
612
+ let scale = 1;
613
+ let translateX = 0;
614
+ let translateY = 0;
615
+ let isDragging = false;
616
+ let startX = 0;
617
+ let startY = 0;
618
+
619
+ // Style the container
620
+ container.style.overflow = 'hidden';
621
+ container.style.cursor = 'grab';
622
+ container.style.position = 'relative';
623
+ svg.style.transformOrigin = 'center center';
624
+ svg.style.transition = 'none';
625
+
626
+ // Create fullscreen modal INSIDE the container (important for fullscreen mode)
627
+ let fsModal = container.querySelector('#fs-detail-modal');
628
+ if (!fsModal) {
629
+ fsModal = document.createElement('div');
630
+ fsModal.id = 'fs-detail-modal';
631
+ fsModal.style.cssText = 'display:none;position:absolute;top:0;right:0;width:350px;height:100%;background:#161b22;z-index:100;overflow-y:auto;border-left:1px solid #30363d;';
632
+ container.appendChild(fsModal);
633
+ }
634
+
635
+ // Make SVG and all children clickable
636
+ svg.style.pointerEvents = 'all';
637
+
638
+ // Direct click on SVG
639
+ svg.addEventListener('click', (e) => {
640
+ console.log('[Diagram] Click detected on:', e.target.tagName, e.target.textContent?.substring(0, 30));
641
+
642
+ // Don't handle if it was dragging
643
+ if (isDragging) return;
644
+
645
+ // Find model name from clicked element or nearby
646
+ let modelName = null;
647
+ let searchEl = e.target;
648
+
649
+ // First check if clicked element itself has model name
650
+ if (searchEl.textContent) {
651
+ const text = searchEl.textContent.trim();
652
+ if (/^[A-Z][a-zA-Z0-9_]+$/.test(text) && text.length > 2) {
653
+ modelName = text;
654
+ }
655
+ }
656
+
657
+ // Search parent elements
658
+ if (!modelName) {
659
+ for (let i = 0; i < 10 && searchEl && searchEl !== svg; i++) {
660
+ // Look for text elements in this group
661
+ const texts = searchEl.querySelectorAll ? searchEl.querySelectorAll('text') : [];
662
+ for (const t of texts) {
663
+ const text = t.textContent?.trim();
664
+ if (text && /^[A-Z][a-zA-Z0-9_]+$/.test(text) && text.length > 2) {
665
+ modelName = text;
666
+ break;
667
+ }
668
+ }
669
+ if (modelName) break;
670
+ searchEl = searchEl.parentElement;
671
+ }
672
+ }
673
+
674
+ console.log('[Diagram] Found model:', modelName);
675
+
676
+ if (modelName) {
677
+ // Show in fullscreen modal if in fullscreen, otherwise normal panel
678
+ if (document.fullscreenElement) {
679
+ console.log('[Diagram] Showing in fullscreen modal');
680
+ showModelDetailInModal(modelName);
681
+ } else {
682
+ console.log('[Diagram] Showing in detail panel');
683
+ showModelDetail(modelName);
684
+ }
685
+ }
686
+ });
687
+
688
+ // Function to show detail in fullscreen modal
689
+ window.showModelDetailInModal = (modelOrName) => {
690
+ let model = modelOrName;
691
+ if (typeof modelOrName === 'string') {
692
+ const normalizedName = modelOrName.replace(/_/g, '');
693
+ model = models.find(m => {
694
+ const className = (m.className || m.name || '').replace(/[^a-zA-Z0-9]/g, '');
695
+ return className.toLowerCase() === normalizedName.toLowerCase();
696
+ });
697
+ }
698
+
699
+ // Find modal inside the container
700
+ const modal = container.querySelector('#fs-detail-modal');
701
+ if (!modal) {
702
+ console.error('[Diagram] Modal not found in container');
703
+ return;
704
+ }
705
+
706
+ if (!model) {
707
+ modal.innerHTML = \`
708
+ <div style="padding:16px;">
709
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
710
+ <strong>Model Not Found</strong>
711
+ <button onclick="document.getElementById('fs-detail-modal').style.display='none'" style="background:none;border:none;color:var(--text-primary);font-size:20px;cursor:pointer;">\xD7</button>
712
+ </div>
713
+ <p>Model "\${modelOrName}" not found.</p>
714
+ </div>
715
+ \`;
716
+ modal.style.display = 'block';
717
+ return;
718
+ }
719
+
720
+ modal.innerHTML = \`
721
+ <div style="padding:16px;">
722
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
723
+ <strong>\${model.className}</strong>
724
+ <button onclick="document.getElementById('fs-detail-modal').style.display='none'" style="background:none;border:none;color:var(--text-primary);font-size:20px;cursor:pointer;">\xD7</button>
725
+ </div>
726
+ <div style="color:var(--text-secondary);margin-bottom:16px;">extends \${model.parentClass}</div>
727
+ \${model.associations.length > 0 ? \`
728
+ <div style="margin-bottom:16px;">
729
+ <div style="font-weight:600;margin-bottom:8px;">Associations (\${model.associations.length})</div>
730
+ \${model.associations.slice(0, 15).map(a => \`
731
+ <div style="padding:4px 0;font-size:13px;">
732
+ <span style="background:var(--accent-blue);padding:2px 6px;border-radius:3px;font-size:11px;margin-right:6px;">\${a.type}</span>
733
+ :\${a.name}
734
+ \${a.through ? \`<small style="color:var(--text-secondary);"> through: \${a.through}</small>\` : ''}
735
+ </div>
736
+ \`).join('')}
737
+ </div>
738
+ \` : ''}
739
+ \${model.validations.length > 0 ? \`
740
+ <div style="margin-bottom:16px;">
741
+ <div style="font-weight:600;margin-bottom:8px;">Validations (\${model.validations.length})</div>
742
+ \${model.validations.slice(0, 10).map(v => \`
743
+ <div style="padding:4px 0;font-size:13px;">
744
+ <span style="background:var(--accent-green);padding:2px 6px;border-radius:3px;font-size:11px;margin-right:6px;">\${v.type}</span>
745
+ \${v.attributes.join(', ')}
746
+ </div>
747
+ \`).join('')}
748
+ </div>
749
+ \` : ''}
750
+ \${model.scopes.length > 0 ? \`
751
+ <div style="margin-bottom:16px;">
752
+ <div style="font-weight:600;margin-bottom:8px;">Scopes (\${model.scopes.length})</div>
753
+ \${model.scopes.map(s => \`<div style="padding:4px 0;font-size:13px;"><span style="background:var(--accent-orange);padding:2px 6px;border-radius:3px;font-size:11px;margin-right:6px;">scope</span>\${s.name}</div>\`).join('')}
754
+ </div>
755
+ \` : ''}
756
+ </div>
757
+ \`;
758
+ modal.style.display = 'block';
759
+ };
760
+
761
+ // Close modal on ESC
762
+ document.addEventListener('keydown', (e) => {
763
+ if (e.key === 'Escape') {
764
+ const modal = container.querySelector('#fs-detail-modal');
765
+ if (modal) modal.style.display = 'none';
766
+ }
767
+ });
768
+
769
+ // Add zoom controls
770
+ const controls = document.createElement('div');
771
+ controls.className = 'diagram-controls';
772
+ controls.innerHTML = \`
773
+ <button onclick="diagramZoom(0.2)" title="Zoom In">+</button>
774
+ <button onclick="diagramZoom(-0.2)" title="Zoom Out">\u2212</button>
775
+ <button onclick="diagramReset()" title="Reset">\u27F2</button>
776
+ <button onclick="diagramFullscreen()" title="Fullscreen" id="fullscreen-btn">\u26F6</button>
777
+ \`;
778
+ controls.style.cssText = 'position:absolute;top:8px;right:8px;display:flex;gap:4px;z-index:10';
779
+ container.appendChild(controls);
780
+
781
+ // Fullscreen change handler
782
+ document.addEventListener('fullscreenchange', () => {
783
+ const btn = document.getElementById('fullscreen-btn');
784
+ if (document.fullscreenElement) {
785
+ if (btn) {
786
+ btn.textContent = '\u26F6';
787
+ btn.title = 'Exit Fullscreen';
788
+ }
789
+ container.style.background = '#1e1e1e';
790
+ } else {
791
+ if (btn) {
792
+ btn.textContent = '\u26F6';
793
+ btn.title = 'Fullscreen';
794
+ }
795
+ container.style.background = '';
796
+ // Hide fullscreen modal when exiting
797
+ const modal = container.querySelector('#fs-detail-modal');
798
+ if (modal) modal.style.display = 'none';
799
+ }
800
+ });
801
+
802
+ function updateTransform() {
803
+ svg.style.transform = \`translate(\${translateX}px, \${translateY}px) scale(\${scale})\`;
804
+ }
805
+
806
+ // Mouse wheel zoom
807
+ container.addEventListener('wheel', (e) => {
808
+ e.preventDefault();
809
+ const delta = e.deltaY > 0 ? -0.1 : 0.1;
810
+ scale = Math.max(0.3, Math.min(3, scale + delta));
811
+ updateTransform();
812
+ }, { passive: false });
813
+
814
+ // Touch pinch zoom
815
+ let lastTouchDist = 0;
816
+ container.addEventListener('touchstart', (e) => {
817
+ if (e.touches.length === 2) {
818
+ lastTouchDist = Math.hypot(
819
+ e.touches[0].clientX - e.touches[1].clientX,
820
+ e.touches[0].clientY - e.touches[1].clientY
821
+ );
822
+ } else if (e.touches.length === 1) {
823
+ isDragging = true;
824
+ startX = e.touches[0].clientX - translateX;
825
+ startY = e.touches[0].clientY - translateY;
826
+ }
827
+ });
828
+
829
+ container.addEventListener('touchmove', (e) => {
830
+ if (e.touches.length === 2) {
831
+ e.preventDefault();
832
+ const dist = Math.hypot(
833
+ e.touches[0].clientX - e.touches[1].clientX,
834
+ e.touches[0].clientY - e.touches[1].clientY
835
+ );
836
+ const delta = (dist - lastTouchDist) * 0.01;
837
+ scale = Math.max(0.3, Math.min(3, scale + delta));
838
+ lastTouchDist = dist;
839
+ updateTransform();
840
+ } else if (e.touches.length === 1 && isDragging) {
841
+ translateX = e.touches[0].clientX - startX;
842
+ translateY = e.touches[0].clientY - startY;
843
+ updateTransform();
844
+ }
845
+ }, { passive: false });
846
+
847
+ container.addEventListener('touchend', () => { isDragging = false; });
848
+
849
+ // Mouse drag pan
850
+ container.addEventListener('mousedown', (e) => {
851
+ isDragging = true;
852
+ container.style.cursor = 'grabbing';
853
+ startX = e.clientX - translateX;
854
+ startY = e.clientY - translateY;
855
+ });
856
+
857
+ container.addEventListener('mousemove', (e) => {
858
+ if (!isDragging) return;
859
+ translateX = e.clientX - startX;
860
+ translateY = e.clientY - startY;
861
+ updateTransform();
862
+ });
863
+
864
+ container.addEventListener('mouseup', () => {
865
+ isDragging = false;
866
+ container.style.cursor = 'grab';
867
+ });
868
+
869
+ container.addEventListener('mouseleave', () => {
870
+ isDragging = false;
871
+ container.style.cursor = 'grab';
872
+ });
873
+
874
+ // Global functions for controls
875
+ window.diagramZoom = (delta) => {
876
+ scale = Math.max(0.3, Math.min(3, scale + delta));
877
+ updateTransform();
878
+ };
879
+
880
+ window.diagramReset = () => {
881
+ scale = 1;
882
+ translateX = 0;
883
+ translateY = 0;
884
+ updateTransform();
885
+ };
886
+
887
+ window.diagramFullscreen = () => {
888
+ if (document.fullscreenElement) {
889
+ document.exitFullscreen();
890
+ } else {
891
+ container.requestFullscreen().catch(err => {
892
+ console.error('Fullscreen error:', err);
893
+ });
894
+ }
895
+ };
896
+ }
897
+
599
898
  function highlightParams(path) {
600
899
  return path.replace(/:([a-zA-Z_]+)/g, '<span class="param">:$1</span>');
601
900
  }
@@ -719,7 +1018,32 @@ import {k}from'./chunk-PTR5IROV.js';import*as r from'fs';import*as c from'path';
719
1018
  \`;
720
1019
  }
721
1020
 
722
- function showModelDetail(model) {
1021
+ function showModelDetail(modelOrName) {
1022
+ // Support both model object and model name string
1023
+ let model = modelOrName;
1024
+ if (typeof modelOrName === 'string') {
1025
+ const normalizedName = modelOrName.replace(/_/g, '');
1026
+ model = models.find(m => {
1027
+ const className = (m.className || m.name || '').replace(/[^a-zA-Z0-9]/g, '');
1028
+ return className.toLowerCase() === normalizedName.toLowerCase();
1029
+ });
1030
+ if (!model) {
1031
+ detailPanel.innerHTML = \`
1032
+ <div class="detail-header">
1033
+ <div class="detail-title">Model Not Found</div>
1034
+ <button class="close-btn" onclick="clearDetail()">\xD7</button>
1035
+ </div>
1036
+ <div class="detail-content">
1037
+ <div class="empty-state">
1038
+ <div>Model "\${modelOrName}" not found in analysis data.</div>
1039
+ </div>
1040
+ </div>
1041
+ \`;
1042
+ detailPanel.classList.add('open');
1043
+ return;
1044
+ }
1045
+ }
1046
+ detailPanel.classList.add('open');
723
1047
  detailPanel.innerHTML = \`
724
1048
  <div class="detail-header">
725
1049
  <div class="detail-title">Model Details</div>
@@ -791,15 +1115,15 @@ import {k}from'./chunk-PTR5IROV.js';import*as r from'fs';import*as c from'path';
791
1115
  renderMainPanel();
792
1116
  </script>
793
1117
  </body>
794
- </html>`}generateNamespaceList(t){let e=new Map;for(let s of t){let i=s.namespace||"root";e.set(i,(e.get(i)||0)+1);}return [...e.entries()].sort((s,i)=>i[1]-s[1]).map(([s,i])=>`
795
- <div class="namespace-item" data-namespace="${s==="root"?"":s}">
796
- <span>${s}</span>
797
- <span class="namespace-count">${i}</span>
1118
+ </html>`}generateNamespaceList(t){let e=new Map;for(let a of t){let l=a.namespace||"root";e.set(l,(e.get(l)||0)+1);}return [...e.entries()].sort((a,l)=>l[1]-a[1]).map(([a,l])=>`
1119
+ <div class="namespace-item" data-namespace="${a==="root"?"":a}">
1120
+ <span>${a}</span>
1121
+ <span class="namespace-count">${l}</span>
798
1122
  </div>
799
- `).join("")}generateMethodFilters(t){let e=["GET","POST","PUT","PATCH","DELETE"],a=new Map;for(let s of t)a.set(s.method,(a.get(s.method)||0)+1);return e.map(s=>`
800
- <div class="namespace-item" data-method="${s}">
801
- <span class="method-badge method-${s}">${s}</span>
802
- <span class="namespace-count">${a.get(s)||0}</span>
1123
+ `).join("")}generateMethodFilters(t){let e=["GET","POST","PUT","PATCH","DELETE"],s=new Map;for(let a of t)s.set(a.method,(s.get(a.method)||0)+1);return e.map(a=>`
1124
+ <div class="namespace-item" data-method="${a}">
1125
+ <span class="method-badge method-${a}">${a}</span>
1126
+ <span class="namespace-count">${s.get(a)||0}</span>
803
1127
  </div>
804
1128
  `).join("")}generateRoutesView(t){return `
805
1129
  <div class="panel-header">
@@ -814,8 +1138,8 @@ import {k}from'./chunk-PTR5IROV.js';import*as r from'fs';import*as c from'path';
814
1138
  </tr>
815
1139
  </thead>
816
1140
  <tbody>
817
- ${t.slice(0,200).map((e,a)=>`
818
- <tr data-type="route" data-index="${a}">
1141
+ ${t.slice(0,200).map((e,s)=>`
1142
+ <tr data-type="route" data-index="${s}">
819
1143
  <td><span class="method-badge method-${e.method}">${e.method}</span></td>
820
1144
  <td class="path-text">${this.highlightParams(e.path)}</td>
821
1145
  <td class="controller-text">${e.controller}#${e.action}</td>