pkgviz 0.7.2 → 0.7.4

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 (94) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +1 -1
  3. package/.next/build-manifest.json +2 -2
  4. package/.next/cache/webpack/client-production/10.pack +0 -0
  5. package/.next/cache/webpack/client-production/7.pack +0 -0
  6. package/.next/cache/webpack/client-production/8.pack +0 -0
  7. package/.next/cache/webpack/client-production/index.pack +0 -0
  8. package/.next/cache/webpack/client-production/index.pack.old +0 -0
  9. package/.next/cache/webpack/server-production/11.pack +0 -0
  10. package/.next/cache/webpack/server-production/12.pack +0 -0
  11. package/.next/cache/webpack/server-production/13.pack +0 -0
  12. package/.next/cache/webpack/server-production/14.pack +0 -0
  13. package/.next/cache/webpack/server-production/15.pack +0 -0
  14. package/.next/cache/webpack/server-production/index.pack +0 -0
  15. package/.next/cache/webpack/server-production/index.pack.old +0 -0
  16. package/.next/server/app/_not-found.html +1 -1
  17. package/.next/server/app/_not-found.rsc +1 -1
  18. package/.next/server/app/favicon.ico/route.js +1 -1
  19. package/.next/server/app/index.html +1 -1
  20. package/.next/server/app/index.rsc +1 -1
  21. package/.next/server/app-paths-manifest.json +1 -1
  22. package/.next/server/chunks/610.js +1 -1
  23. package/.next/server/pages/404.html +1 -1
  24. package/.next/server/pages/500.html +1 -1
  25. package/.next/trace +2 -2
  26. package/package.json +3 -3
  27. package/src/app/actions/graph.actions.ts +25 -0
  28. package/src/app/favicon.ico +0 -0
  29. package/src/app/globals.css +77 -0
  30. package/src/app/layout.tsx +30 -0
  31. package/src/app/page.tsx +5 -0
  32. package/src/app/utils/buildGraph.ts +119 -0
  33. package/src/app/utils/getParsedFileStructure.ts +225 -0
  34. package/src/app/utils/markCyclicPackages.ts +275 -0
  35. package/src/app/utils/parser/cpp/extractCppPackageFromImport.ts +18 -0
  36. package/src/app/utils/parser/cpp/parseCppFile.ts +150 -0
  37. package/src/app/utils/parser/delphi/extractPackageFromImport.ts +21 -0
  38. package/src/app/utils/parser/delphi/parseFile.ts +179 -0
  39. package/src/app/utils/parser/java/extractJavaPackageFromImport.ts +39 -0
  40. package/src/app/utils/parser/java/findEntryPoint.ts +24 -0
  41. package/src/app/utils/parser/java/getIntrinsicPackagesRecursive.ts +33 -0
  42. package/src/app/utils/parser/java/parseJavaFile.ts +114 -0
  43. package/src/app/utils/parser/kotlin/extractPackageFromImport.ts +19 -0
  44. package/src/app/utils/parser/kotlin/parseFile.ts +147 -0
  45. package/src/app/utils/parser/python/extractPythonPackageFromImport.ts +18 -0
  46. package/src/app/utils/parser/python/parseFile.ts +171 -0
  47. package/src/app/utils/parser/typescript/extractTypeScriptPackageFromImport.ts +18 -0
  48. package/src/app/utils/parser/typescript/parseFile.ts +130 -0
  49. package/src/components/Breadcrumb.tsx +34 -0
  50. package/src/components/Cytoscape.tsx +23 -0
  51. package/src/components/Header.tsx +28 -0
  52. package/src/components/Loader.tsx +10 -0
  53. package/src/components/Setting.tsx +17 -0
  54. package/src/components/Settings.tsx +189 -0
  55. package/src/components/Switch.tsx +31 -0
  56. package/src/components/ThemeToggle.tsx +25 -0
  57. package/src/components/ZoomInput.tsx +94 -0
  58. package/src/components/useCytoscape.ts +343 -0
  59. package/src/contexts/SettingsContext.tsx +88 -0
  60. package/src/i18n/en.ts +27 -0
  61. package/src/i18n/i18n.ts +12 -0
  62. package/src/layouts/breadthfirst/layout.ts +30 -0
  63. package/src/layouts/breadthfirst/style.ts +8 -0
  64. package/src/layouts/circle/layout.ts +11 -0
  65. package/src/layouts/circle/style.ts +18 -0
  66. package/src/layouts/concentric/layout.ts +10 -0
  67. package/src/layouts/concentric/style.ts +16 -0
  68. package/src/layouts/constants.ts +17 -0
  69. package/src/layouts/elk/layout.ts +55 -0
  70. package/src/layouts/elk/style.ts +14 -0
  71. package/src/layouts/getLayoutStyle.ts +19 -0
  72. package/src/layouts/getWeightBuckets.ts +58 -0
  73. package/src/layouts/grid/layout.ts +11 -0
  74. package/src/layouts/grid/style.ts +20 -0
  75. package/src/layouts/index.ts +14 -0
  76. package/src/layouts/style.ts +191 -0
  77. package/src/screens/home/Home.tsx +48 -0
  78. package/src/shared/constants/index.ts +7 -0
  79. package/src/shared/types/index.ts +68 -0
  80. package/src/shared/utils/detectLanguage.ts +255 -0
  81. package/src/shared/utils/getJsonAsync.ts +13 -0
  82. package/src/shared/utils/getProjectName.ts +3 -0
  83. package/src/shared/utils/parseEnv.ts +91 -0
  84. package/src/shared/utils/parseProjectPath.ts +8 -0
  85. package/src/store/useLocalStorage.ts +29 -0
  86. package/src/utils/filter/filterByPackagePrefix.ts +23 -0
  87. package/src/utils/filter/filterEmptyPackages.ts +36 -0
  88. package/src/utils/filter/filterSubPackagesFromDepth.ts +170 -0
  89. package/src/utils/filter/filterVendorPackages.ts +17 -0
  90. package/src/utils/filter/toggleCompoundNodes.ts +40 -0
  91. package/src/utils/hasChildren.ts +7 -0
  92. package/tsconfig.json +29 -0
  93. /package/.next/static/{VVBfFRai9p1x3oVXujjYO → F9v-zmBCEef1qcQb8JFxR}/_buildManifest.js +0 -0
  94. /package/.next/static/{VVBfFRai9p1x3oVXujjYO → F9v-zmBCEef1qcQb8JFxR}/_ssgManifest.js +0 -0
@@ -0,0 +1,225 @@
1
+ 'use server';
2
+ import { Language, type ParsedDirectory } from '@/shared/types';
3
+
4
+ import { existsSync, readdirSync } from 'node:fs';
5
+ import path from 'node:path';
6
+ import { toPosix } from '@/shared/utils/toPosix';
7
+ import { detectLanguage } from '@/shared/utils/detectLanguage';
8
+ import { parseJavaFile } from '@/app/utils/parser/java/parseJavaFile';
9
+ import { parseFile as parseTypeScriptFile } from '@/app/utils/parser/typescript/parseFile';
10
+ import { parseCppFile } from '@/app/utils/parser/cpp/parseCppFile';
11
+ import { parsePythonFile } from '@/app/utils/parser/python/parseFile';
12
+ import { parseDelphiFile } from '@/app/utils/parser/delphi/parseFile';
13
+ import { parseKotlinFile } from '@/app/utils/parser/kotlin/parseFile';
14
+ import { parseProjectPath } from '@/shared/utils/parseProjectPath';
15
+ import { JAVA_ROOT } from '@/shared/constants';
16
+
17
+ /**
18
+ * Returns resolved root
19
+ */
20
+ export async function resolveRoot(dir: string, detectedLanguage: Language) {
21
+ switch (detectedLanguage) {
22
+ case Language.Java: {
23
+ const javaRoot = toPosix(path.resolve(dir, JAVA_ROOT));
24
+ if (!existsSync(javaRoot)) {
25
+ console.error('Failed to find:', JAVA_ROOT);
26
+ throw new Error(`Invalid Java project structure. Missing ${JAVA_ROOT}`);
27
+ }
28
+ return javaRoot;
29
+ }
30
+
31
+ case Language.TypeScript:
32
+ // Normalize to an absolute project root
33
+ return toPosix(path.resolve(dir));
34
+
35
+ case Language.Cpp:
36
+ // For C++, look for src directory or use project root
37
+ const cppSrcRoot = toPosix(path.resolve(dir, 'src'));
38
+ if (existsSync(cppSrcRoot)) {
39
+ return cppSrcRoot;
40
+ }
41
+ return toPosix(path.resolve(dir));
42
+
43
+ case Language.Python:
44
+ // For Python, look for src directory or use project root
45
+ const pythonSrcRoot = toPosix(path.resolve(dir, 'src'));
46
+ if (existsSync(pythonSrcRoot)) {
47
+ return pythonSrcRoot;
48
+ }
49
+ // Also check for common Python app structure
50
+ const appRoot = toPosix(path.resolve(dir, 'app'));
51
+ if (existsSync(appRoot)) {
52
+ return appRoot;
53
+ }
54
+ return toPosix(path.resolve(dir));
55
+
56
+ case Language.Delphi:
57
+ // For Delphi, look for common source directories
58
+ const delphiSrcRoot = toPosix(path.resolve(dir, 'src'));
59
+ if (existsSync(delphiSrcRoot)) {
60
+ return delphiSrcRoot;
61
+ }
62
+ // Also check for Source directory (common in Delphi projects)
63
+ const sourceRoot = toPosix(path.resolve(dir, 'Source'));
64
+ if (existsSync(sourceRoot)) {
65
+ return sourceRoot;
66
+ }
67
+ return toPosix(path.resolve(dir));
68
+
69
+ case Language.Kotlin:
70
+ // For Kotlin, look for src/main/kotlin directory (Gradle/Maven structure)
71
+ const kotlinSrcRoot = toPosix(path.resolve(dir, 'src/main/kotlin'));
72
+ if (existsSync(kotlinSrcRoot)) {
73
+ return kotlinSrcRoot;
74
+ }
75
+ // Fallback to src directory
76
+ const kotlinAltSrcRoot = toPosix(path.resolve(dir, 'src'));
77
+ if (existsSync(kotlinAltSrcRoot)) {
78
+ return kotlinAltSrcRoot;
79
+ }
80
+ return toPosix(path.resolve(dir));
81
+
82
+ default:
83
+ throw new Error(`Invalid file structure for ${detectedLanguage}`);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Read directory recursively
89
+ */
90
+ export async function readDirRecursively(
91
+ dir: string,
92
+ result: ParsedDirectory = {},
93
+ projectRoot: string,
94
+ language: Language
95
+ ): Promise<ParsedDirectory> {
96
+ const resolvedRoot = path.resolve(projectRoot);
97
+ const resolvedDir = path.resolve(dir);
98
+
99
+ // Validate dir is inside projectRoot (avoid path traversal / accidental escapes)
100
+ const relative = path.relative(resolvedRoot, resolvedDir);
101
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
102
+ throw new Error(`Path traversal detected: ${dir} is outside of project root ${projectRoot}`);
103
+ }
104
+
105
+ // 1. Read current directory
106
+ const entries = readdirSync(resolvedDir, { withFileTypes: true });
107
+
108
+ const ignores = [
109
+ '@types',
110
+ '.cache',
111
+ '.git',
112
+ '.github',
113
+ '.meta',
114
+ '.next',
115
+ '.vscode',
116
+ 'coverage',
117
+ 'examples',
118
+ 'node_modules',
119
+ 'packages',
120
+ 'test',
121
+ ];
122
+
123
+ for (const entry of entries) {
124
+ if (ignores.includes(entry.name)) continue;
125
+
126
+ const fullPath = path.resolve(resolvedDir, entry.name);
127
+
128
+ // Directory: Recursively continue to read
129
+ if (entry.isDirectory()) {
130
+ result[entry.name] = await readDirRecursively(fullPath, {}, projectRoot, language);
131
+ continue;
132
+ }
133
+
134
+ // File: Parse file according to detected project language
135
+ switch (language) {
136
+ // Java
137
+ case Language.Java:
138
+ if (entry.name.endsWith('.java')) {
139
+ result[entry.name] = await parseJavaFile(fullPath, projectRoot);
140
+ }
141
+ break;
142
+
143
+ // TypeScript
144
+ case Language.TypeScript:
145
+ if (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx')) {
146
+ result[entry.name] = await parseTypeScriptFile(fullPath, projectRoot);
147
+ }
148
+ break;
149
+
150
+ // C++
151
+ case Language.Cpp:
152
+ if (
153
+ entry.name.endsWith('.cpp') ||
154
+ entry.name.endsWith('.cc') ||
155
+ entry.name.endsWith('.cxx') ||
156
+ entry.name.endsWith('.h') ||
157
+ entry.name.endsWith('.hpp') ||
158
+ entry.name.endsWith('.hxx')
159
+ ) {
160
+ result[entry.name] = await parseCppFile(fullPath, projectRoot);
161
+ }
162
+ break;
163
+
164
+ // Python
165
+ case Language.Python:
166
+ if (entry.name.endsWith('.py')) {
167
+ result[entry.name] = await parsePythonFile(fullPath, projectRoot);
168
+ }
169
+ break;
170
+
171
+ // Delphi
172
+ case Language.Delphi:
173
+ if (
174
+ entry.name.endsWith('.pas') ||
175
+ entry.name.endsWith('.pp') ||
176
+ entry.name.endsWith('.dpr')
177
+ ) {
178
+ result[entry.name] = await parseDelphiFile(fullPath, projectRoot);
179
+ }
180
+ break;
181
+
182
+ // Kotlin
183
+ case Language.Kotlin:
184
+ if (entry.name.endsWith('.kt') || entry.name.endsWith('.kts')) {
185
+ result[entry.name] = await parseKotlinFile(fullPath, projectRoot);
186
+ }
187
+ break;
188
+ }
189
+ }
190
+
191
+ return result;
192
+ }
193
+
194
+ /**
195
+ * Entrypoint
196
+ */
197
+ export async function getParsedFileStructure() {
198
+ const projectPath = parseProjectPath();
199
+
200
+ // 1. Detect language & filter non-supported
201
+ const detectedLanguage = (await detectLanguage(projectPath)).language;
202
+ console.log('1. Detected language:', detectedLanguage);
203
+
204
+ if (
205
+ ![
206
+ Language.Java,
207
+ Language.TypeScript,
208
+ Language.Cpp,
209
+ Language.Python,
210
+ Language.Delphi,
211
+ Language.Kotlin,
212
+ ].includes(detectedLanguage)
213
+ ) {
214
+ throw new Error(
215
+ "Supported language is 'Java', 'TypeScript', 'C++', 'Python', 'Delphi' & 'Kotlin'. More to follow."
216
+ );
217
+ }
218
+
219
+ // 2. Get validated root directory by detectedLanguage
220
+ const rootDir = await resolveRoot(projectPath, detectedLanguage);
221
+ console.log('2. rootDir:', rootDir);
222
+
223
+ // 3. Read directory recursively (pass resolved root as both dir and projectRoot)
224
+ return await readDirRecursively(rootDir, {}, rootDir, detectedLanguage);
225
+ }
@@ -0,0 +1,275 @@
1
+ import type { ElementsDefinition } from 'cytoscape';
2
+ import type { ParsedDirectory, ParsedFile } from '@/shared/types';
3
+
4
+ /** Collect all files from your ParsedDirectory tree. */
5
+ function collectFiles(root: ParsedDirectory): ParsedFile[] {
6
+ const files: ParsedFile[] = [];
7
+ const walk = (dir: ParsedDirectory) => {
8
+ for (const key in dir) {
9
+ const entry = (dir as Record<string, unknown>)[key];
10
+ if (entry && typeof entry === 'object' && 'path' in entry && 'package' in entry) {
11
+ files.push(entry as ParsedFile);
12
+ } else if (entry && typeof entry === 'object') {
13
+ walk(entry as ParsedDirectory);
14
+ }
15
+ }
16
+ };
17
+ walk(root);
18
+ return files;
19
+ }
20
+
21
+ /** Build per-edge evidence: key "from->to" → list of ImportEvidence. */
22
+ function buildEdgeEvidence(dir: ParsedDirectory): Map<string, ImportEvidence[]> {
23
+ const files = collectFiles(dir);
24
+ const map = new Map<string, ImportEvidence[]>();
25
+
26
+ for (const f of files) {
27
+ const from = f.package as TUniquePackageName;
28
+ for (const imp of f.imports ?? []) {
29
+ const to = imp.pkg as TUniquePackageName;
30
+ if (!to || to === from) continue;
31
+ const key = `${from}->${to}`;
32
+ if (!map.has(key)) map.set(key, []);
33
+ map.get(key)!.push({
34
+ filePath: f.path,
35
+ fileClass: f.className,
36
+ importName: imp.name,
37
+ isIntrinsic: imp.isIntrinsic,
38
+ });
39
+ }
40
+ }
41
+ return map;
42
+ }
43
+
44
+ /** Convert ElementsDefinition (from buildGraph) to adjacency map. */
45
+ function elementsToAdj(elements: ElementsDefinition) {
46
+ const adj = new Map<TUniquePackageName, Set<TUniquePackageName>>();
47
+ for (const n of elements.nodes) {
48
+ const id = String(n.data.id) as TUniquePackageName;
49
+ if (!adj.has(id)) adj.set(id, new Set());
50
+ }
51
+ for (const e of elements.edges) {
52
+ const s = String(e.data.source) as TUniquePackageName;
53
+ const t = String(e.data.target) as TUniquePackageName;
54
+ if (!adj.has(s)) adj.set(s, new Set());
55
+ if (!adj.has(t)) adj.set(t, new Set());
56
+ adj.get(s)!.add(t);
57
+ }
58
+ return adj;
59
+ }
60
+
61
+ /** Tarjan SCC on adjacency. */
62
+ function tarjanSCC(
63
+ graph: Map<TUniquePackageName, Set<TUniquePackageName>>
64
+ ): TUniquePackageName[][] {
65
+ let index = 0;
66
+ const idx = new Map<TUniquePackageName, number>();
67
+ const low = new Map<TUniquePackageName, number>();
68
+ const stack: TUniquePackageName[] = [];
69
+ const onStack = new Set<TUniquePackageName>();
70
+ const out: TUniquePackageName[][] = [];
71
+
72
+ function strong(v: TUniquePackageName) {
73
+ idx.set(v, index);
74
+ low.set(v, index);
75
+ index++;
76
+ stack.push(v);
77
+ onStack.add(v);
78
+
79
+ for (const w of graph.get(v) ?? []) {
80
+ if (!idx.has(w)) {
81
+ strong(w);
82
+ low.set(v, Math.min(low.get(v)!, low.get(w)!));
83
+ } else if (onStack.has(w)) {
84
+ low.set(v, Math.min(low.get(v)!, idx.get(w)!));
85
+ }
86
+ }
87
+
88
+ if (low.get(v) === idx.get(v)) {
89
+ const comp: TUniquePackageName[] = [];
90
+ let w: TUniquePackageName;
91
+ do {
92
+ w = stack.pop()!;
93
+ onStack.delete(w);
94
+ comp.push(w);
95
+ } while (w !== v);
96
+ out.push(comp);
97
+ }
98
+ }
99
+
100
+ for (const v of graph.keys()) if (!idx.has(v)) strong(v);
101
+ return out;
102
+ }
103
+
104
+ /** Find one simple cycle ordering inside a given SCC. */
105
+ function findOneCycleInScc(
106
+ graph: Map<TUniquePackageName, Set<TUniquePackageName>>,
107
+ sccSet: Set<TUniquePackageName>
108
+ ): TUniquePackageName[] | null {
109
+ const nodes = Array.from(sccSet);
110
+ for (const start of nodes) {
111
+ const path: TUniquePackageName[] = [];
112
+ const seen = new Set<TUniquePackageName>();
113
+ function dfs(v: TUniquePackageName): TUniquePackageName[] | null {
114
+ path.push(v);
115
+ seen.add(v);
116
+ for (const w of graph.get(v) ?? []) {
117
+ if (!sccSet.has(w)) continue;
118
+ if (w === start && path.length > 1) return [...path, start];
119
+ if (!seen.has(w)) {
120
+ const r = dfs(w);
121
+ if (r) return r;
122
+ }
123
+ }
124
+ path.pop();
125
+ return null;
126
+ }
127
+ const cyc = dfs(start);
128
+ if (cyc) return cyc;
129
+ }
130
+ return null;
131
+ }
132
+
133
+ /**
134
+ * High-level API: build graph via `buildGraph`, detect package cycles,
135
+ * and attach **member evidence** (files/imports) per cycle edge.
136
+ */
137
+ export function getPackageCyclesWithMembers(
138
+ dir: ParsedDirectory,
139
+ graph: ElementsDefinition
140
+ ): {
141
+ cycles: PackageCycleDetail[];
142
+ packageSet: Set<TUniquePackageName>;
143
+ graph: ElementsDefinition; // for convenience (already built)
144
+ } {
145
+ const adj = elementsToAdj(graph);
146
+ const sccs = tarjanSCC(adj);
147
+ const evidence = buildEdgeEvidence(dir);
148
+
149
+ const cycles: PackageCycleDetail[] = [];
150
+ const packageSet = new Set<TUniquePackageName>();
151
+
152
+ for (const scc of sccs) {
153
+ const selfLoop = scc.length === 1 && (adj.get(scc[0])?.has(scc[0]) ?? false);
154
+ if (scc.length > 1 || selfLoop) {
155
+ scc.forEach(p => packageSet.add(p));
156
+ const sccSet = new Set(scc);
157
+ const cycle = findOneCycleInScc(adj, sccSet) ?? [...scc, scc[0]];
158
+
159
+ const edges: CycleEdgeEvidence[] = [];
160
+ for (let i = 0; i < cycle.length - 1; i++) {
161
+ const from = cycle[i];
162
+ const to = cycle[i + 1];
163
+ const key = `${from}->${to}`;
164
+ edges.push({
165
+ from,
166
+ to,
167
+ via: evidence.get(key) ?? [],
168
+ });
169
+ }
170
+
171
+ cycles.push({ packages: cycle, edges });
172
+ }
173
+ }
174
+
175
+ return { cycles, packageSet, graph };
176
+ }
177
+
178
+ /**
179
+ * Annotate Cytoscape nodes:
180
+ * - adds class "packageCycle" for cyclic packages
181
+ * - sets data.packageCycle (boolean)
182
+ * - sets data.cycleEvidence (edges where this pkg is the "from" side)
183
+ */
184
+ export function markCyclicPackagesWithEvidence(
185
+ elements: ElementsDefinition,
186
+ dir: ParsedDirectory
187
+ ): ElementsDefinition {
188
+ const { cycles, packageSet } = getPackageCyclesWithMembers(dir, elements);
189
+
190
+ // pkg → list of outgoing cycle edges (evidence)
191
+ const pkgToEdges = new Map<TUniquePackageName, CycleEdgeEvidence[]>();
192
+
193
+ // Track cycle edges by (from → to) for quick lookup when mapping Cytoscape edges
194
+ const cycleEdgeKeys = new Set<string>();
195
+
196
+ for (const c of cycles) {
197
+ for (const e of c.edges) {
198
+ if (!pkgToEdges.has(e.from)) pkgToEdges.set(e.from, []);
199
+ pkgToEdges.get(e.from)!.push(e);
200
+ cycleEdgeKeys.add(`${e.from}→${e.to}`);
201
+ }
202
+ }
203
+
204
+ const nodes = (elements.nodes ?? []).map(n => {
205
+ const id = String(n.data.id) as TUniquePackageName;
206
+ const isCyclic = packageSet.has(id);
207
+ const existing = n.classes ? String(n.classes) : '';
208
+ const classes = isCyclic ? (existing ? `${existing} packageCycle` : 'packageCycle') : existing;
209
+
210
+ return {
211
+ ...n,
212
+ classes,
213
+ data: {
214
+ ...n.data,
215
+ packageCycle: isCyclic,
216
+ cycleEvidence: isCyclic ? (pkgToEdges.get(id) ?? []) : undefined,
217
+ },
218
+ };
219
+ });
220
+
221
+ const edges = (elements.edges ?? []).map(e => {
222
+ const source = String(e.data.source) as TUniquePackageName;
223
+ const target = String(e.data.target) as TUniquePackageName;
224
+ const isCycleEdge = cycleEdgeKeys.has(`${source}→${target}`);
225
+
226
+ const existing = e.classes ? String(e.classes) : '';
227
+ const classes = isCycleEdge
228
+ ? existing
229
+ ? `${existing} packageCycle`
230
+ : 'packageCycle'
231
+ : existing;
232
+
233
+ return {
234
+ ...e,
235
+ classes,
236
+ data: {
237
+ ...e.data,
238
+ packageCycle: isCycleEdge,
239
+ },
240
+ };
241
+ });
242
+
243
+ return { ...elements, nodes, edges };
244
+ }
245
+
246
+ /**
247
+ * Convenience that only returns the set of cyclic packages (no evidence)
248
+ */
249
+ export function getCyclicPackageSet(
250
+ dir: ParsedDirectory,
251
+ graph: ElementsDefinition
252
+ ): Set<TUniquePackageName> {
253
+ const { packageSet } = getPackageCyclesWithMembers(dir, graph);
254
+ return packageSet;
255
+ }
256
+
257
+ type TUniquePackageName = string;
258
+
259
+ interface ImportEvidence {
260
+ readonly filePath: string; // ParsedFile.path (relative to project)
261
+ readonly fileClass: string; // ParsedFile.className
262
+ readonly importName: string; // IJavaImport.name
263
+ readonly isIntrinsic?: boolean; // IJavaImport.isIntrinsic
264
+ }
265
+
266
+ interface CycleEdgeEvidence {
267
+ readonly from: TUniquePackageName;
268
+ readonly to: TUniquePackageName;
269
+ readonly via: ImportEvidence[]; // files in `from` that import `to`
270
+ }
271
+
272
+ export interface PackageCycleDetail {
273
+ readonly packages: TUniquePackageName[]; // ordered cycle incl. closing node, e.g. ["A","B","A"]
274
+ readonly edges: CycleEdgeEvidence[]; // evidence aligned with packages[i] -> packages[i+1]
275
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Extracts the package/namespace from a C++ include statement
3
+ * For example: "myapp/utils/Helper.h" -> "myapp.utils"
4
+ */
5
+ export function extractCppPackageFromImport(importPath: string): string {
6
+ // Remove file extension
7
+ const withoutExt = importPath.replace(/\.(h|hpp|hxx)$/, '');
8
+
9
+ // Split by path separator and take all but the last segment
10
+ const segments = withoutExt.split('/');
11
+
12
+ if (segments.length <= 1) {
13
+ return '';
14
+ }
15
+
16
+ // Join with dots to create package-like structure
17
+ return segments.slice(0, -1).join('.');
18
+ }
@@ -0,0 +1,150 @@
1
+ 'use server';
2
+ import type { ParsedFile, MethodCall, MethodDefinition, ImportDefinition } from '@/shared/types';
3
+
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { extractCppPackageFromImport } from '@/app/utils/parser/cpp/extractCppPackageFromImport';
7
+
8
+ /**
9
+ * Extracts namespace from C++ code.
10
+ */
11
+ function extractNamespace(content: string): string {
12
+ const match = content.match(/namespace\s+([a-zA-Z0-9_:]+)\s*\{/);
13
+ return match?.[1]?.replace(/::/g, '.') || '';
14
+ }
15
+
16
+ /**
17
+ * Extracts include statements from C++ code.
18
+ */
19
+ function extractIncludes(content: string, projectRoot: string): ImportDefinition[] {
20
+ console.log('[CPP] projectRoot:', projectRoot);
21
+ const includeRegex = /#include\s+["<]([^">]+)[">]/g;
22
+ const includes: ImportDefinition[] = [];
23
+
24
+ let match;
25
+ while ((match = includeRegex.exec(content)) !== null) {
26
+ const includePath = match[1];
27
+
28
+ // System includes use angle brackets, local includes use quotes
29
+ const isSystemInclude = content.includes(`<${includePath}>`);
30
+ const pkg = extractCppPackageFromImport(includePath);
31
+
32
+ includes.push({
33
+ name: includePath,
34
+ pkg,
35
+ isIntrinsic: !isSystemInclude, // Local includes are intrinsic
36
+ });
37
+ }
38
+
39
+ return includes;
40
+ }
41
+
42
+ /**
43
+ * Extracts the class name from the content and filename fallback.
44
+ */
45
+ function extractClassName(content: string, fileName: string): string {
46
+ // Try to find class declaration
47
+ const classMatch = content.match(/class\s+([A-Za-z0-9_]+)/);
48
+ if (classMatch) {
49
+ return classMatch[1];
50
+ }
51
+
52
+ // Try to find struct declaration
53
+ const structMatch = content.match(/struct\s+([A-Za-z0-9_]+)/);
54
+ if (structMatch) {
55
+ return structMatch[1];
56
+ }
57
+
58
+ // Fallback to filename without extension
59
+ return path.basename(fileName, path.extname(fileName));
60
+ }
61
+
62
+ /**
63
+ * Extracts method definitions from C++ content.
64
+ */
65
+ function extractMethodDefinitions(content: string): MethodDefinition[] {
66
+ const methods: MethodDefinition[] = [];
67
+
68
+ // Match method definitions (simplified regex, may need refinement)
69
+ const methodRegex = /(?:(public|protected|private):\s*)?([\w<>:&*\s]+)\s+(\w+)\s*$$([^)]*)$$/g;
70
+
71
+ let match;
72
+ let currentVisibility: 'public' | 'protected' | 'private' | 'default' = 'default';
73
+
74
+ // Also track visibility changes
75
+ const visibilityRegex = /(public|protected|private):/g;
76
+ const lines = content.split('\n');
77
+
78
+ for (const line of lines) {
79
+ const visMatch = visibilityRegex.exec(line);
80
+ if (visMatch) {
81
+ currentVisibility = visMatch[1] as 'public' | 'protected' | 'private';
82
+ }
83
+ }
84
+
85
+ methodRegex.lastIndex = 0;
86
+ while ((match = methodRegex.exec(content)) !== null) {
87
+ const visibility = (match[1] as 'public' | 'protected' | 'private') || currentVisibility;
88
+ const returnType = match[2]?.trim() || 'void';
89
+ const name = match[3];
90
+ const params = match[4]
91
+ .split(',')
92
+ .map(p => p.trim())
93
+ .filter(Boolean);
94
+
95
+ // Skip obvious non-methods (keywords, control structures)
96
+ if (['if', 'while', 'for', 'switch', 'catch'].includes(name)) {
97
+ continue;
98
+ }
99
+
100
+ methods.push({
101
+ name,
102
+ returnType,
103
+ parameters: params,
104
+ visibility,
105
+ });
106
+ }
107
+
108
+ return methods;
109
+ }
110
+
111
+ /**
112
+ * Extract method calls from C++ content.
113
+ */
114
+ function extractMethodCalls(content: string): MethodCall[] {
115
+ const callRegex = /(\b\w+)(?:\.|->)(\w+)\s*\(/g;
116
+ const calls: MethodCall[] = [];
117
+
118
+ let match;
119
+ while ((match = callRegex.exec(content)) !== null) {
120
+ const callee = match[1];
121
+ const method = match[2];
122
+ calls.push({ callee, method });
123
+ }
124
+
125
+ return calls;
126
+ }
127
+
128
+ /**
129
+ * Parses a C++ file and returns metadata useful for diagram generation.
130
+ */
131
+ export async function parseCppFile(fullPath: string, projectRoot: string): Promise<ParsedFile> {
132
+ const content = fs.readFileSync(fullPath, 'utf-8');
133
+ const fileName = path.basename(fullPath);
134
+
135
+ const className = extractClassName(content, fileName);
136
+ const namespace = extractNamespace(content);
137
+ const includes = extractIncludes(content, projectRoot);
138
+ const methods = extractMethodDefinitions(content);
139
+ const calls = extractMethodCalls(content);
140
+ const relativePath = path.relative(projectRoot, fullPath);
141
+
142
+ return {
143
+ className,
144
+ package: namespace,
145
+ imports: includes,
146
+ methods,
147
+ calls,
148
+ path: relativePath,
149
+ };
150
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Extracts the package/unit name from a Delphi uses clause import.
3
+ *
4
+ * Delphi imports are in the format:
5
+ * - uses UnitName;
6
+ * - uses UnitName in 'path/to/UnitName.pas';
7
+ * - uses Vcl.Forms, System.SysUtils;
8
+ *
9
+ * @param importStatement - The full import statement (e.g., "System.SysUtils")
10
+ * @returns The package/namespace name
11
+ */
12
+ export function extractPackageFromImport(importStatement: string): string {
13
+ const trimmed = importStatement.trim();
14
+
15
+ // Handle dotted notation (e.g., "System.SysUtils" -> "System")
16
+ const parts = trimmed.split('.');
17
+ if (parts.length > 1) return parts[0];
18
+
19
+ // For single unit names without namespace, return as-is
20
+ return trimmed;
21
+ }