@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.
- package/README.md +61 -13
- package/dist/analyzers/index.d.ts +175 -25
- package/dist/analyzers/index.js +1 -1
- package/dist/{chunk-QZWPOG5B.js → chunk-GCIRJGW3.js} +78 -45
- package/dist/chunk-H7VVRHQZ.js +34 -0
- package/dist/chunk-HPBPEGHS.js +19 -0
- package/dist/{chunk-WQANJ7IA.js → chunk-JDM7Y7PX.js} +34 -28
- package/dist/{chunk-H4YGP3GL.js → chunk-OQAXO3X2.js} +346 -22
- package/dist/chunk-TNUKDIO7.js +5 -0
- package/dist/cli.js +21 -35
- package/dist/dataflow-analyzer-CJ2T0cGS.d.ts +345 -0
- package/dist/generators/assets/docs.css +176 -46
- package/dist/generators/assets/favicon/apple-touch-icon.png +0 -0
- package/dist/generators/assets/favicon/favicon-96x96.png +0 -0
- package/dist/generators/assets/favicon/favicon.ico +0 -0
- package/dist/generators/assets/favicon/favicon.svg +3 -0
- package/dist/generators/assets/favicon/site.webmanifest +21 -0
- package/dist/generators/assets/favicon/web-app-manifest-192x192.png +0 -0
- package/dist/generators/assets/favicon/web-app-manifest-512x512.png +0 -0
- package/dist/generators/assets/page-map.css +392 -87
- package/dist/generators/assets/rails-map.css +221 -48
- package/dist/generators/index.d.ts +0 -8
- package/dist/generators/index.js +1 -1
- package/dist/index.d.ts +18 -9
- package/dist/index.js +1 -1
- package/dist/page-map-generator-3GO6GL2P.js +1 -0
- package/dist/{rails-FFISZ4AE.js → rails-3HNUFTQV.js} +1 -1
- package/dist/rails-map-generator-CAQZUBI6.js +1 -0
- package/dist/server/index.d.ts +2 -6
- package/dist/server/index.js +1 -1
- package/dist/types.d.ts +12 -3
- package/package.json +1 -5
- package/dist/chunk-BPV4UZSW.js +0 -2
- package/dist/chunk-PTR5IROV.js +0 -36
- package/dist/chunk-XWZH2RDG.js +0 -19
- package/dist/dataflow-analyzer-s6ufFkKC.d.ts +0 -215
- package/dist/page-map-generator-HBKSOX2E.js +0 -1
- 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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
<
|
|
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 = ${
|
|
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(
|
|
101
|
-
<a href="/docs/repos/${
|
|
102
|
-
<a href="/docs/repos/${
|
|
103
|
-
<a href="/docs/repos/${
|
|
104
|
-
<a href="/docs/repos/${
|
|
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="
|
|
161
|
-
<button onclick="zoomDiagram(\${idx}, 1.25)" title="
|
|
162
|
-
<button onclick="zoomDiagram(\${idx}, 'reset')" title="
|
|
163
|
-
<button onclick="toggleFullscreen(\${idx})" title="
|
|
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
|
|
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('
|
|
944
|
+
alert('Generation failed: ' + data.error);
|
|
938
945
|
}
|
|
939
946
|
} catch (e) {
|
|
940
|
-
alert('
|
|
947
|
+
alert('Error: ' + e.message);
|
|
941
948
|
} finally {
|
|
942
949
|
const btn = document.querySelector('.regenerate-btn');
|
|
943
|
-
btn.textContent = '\u{1F504}
|
|
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
|
|
950
|
-
\
|
|
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
|
|
957
|
-
\u{1F44B} Server stopped`);}};export{
|
|
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-
|
|
2
|
-
\u{1F4C4} Generated: ${t.outputPath}`)),
|
|
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">${
|
|
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">${
|
|
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">${
|
|
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">${
|
|
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 (${
|
|
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(
|
|
94
|
-
const models = ${JSON.stringify(
|
|
95
|
-
const grpcServices = ${JSON.stringify(
|
|
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(
|
|
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
|
|
795
|
-
<div class="namespace-item" data-namespace="${
|
|
796
|
-
<span>${
|
|
797
|
-
<span class="namespace-count">${
|
|
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"],
|
|
800
|
-
<div class="namespace-item" data-method="${
|
|
801
|
-
<span class="method-badge method-${
|
|
802
|
-
<span class="namespace-count">${
|
|
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,
|
|
818
|
-
<tr data-type="route" data-index="${
|
|
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>
|