pkgviz 0.7.1 → 0.7.3

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 (92) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +2 -2
  3. package/.next/cache/webpack/client-production/4.pack +0 -0
  4. package/.next/cache/webpack/client-production/5.pack +0 -0
  5. package/.next/cache/webpack/client-production/6.pack +0 -0
  6. package/.next/cache/webpack/client-production/7.pack +0 -0
  7. package/.next/cache/webpack/client-production/8.pack +0 -0
  8. package/.next/cache/webpack/client-production/index.pack +0 -0
  9. package/.next/cache/webpack/client-production/index.pack.old +0 -0
  10. package/.next/cache/webpack/server-production/10.pack +0 -0
  11. package/.next/cache/webpack/server-production/11.pack +0 -0
  12. package/.next/cache/webpack/server-production/12.pack +0 -0
  13. package/.next/cache/webpack/server-production/13.pack +0 -0
  14. package/.next/cache/webpack/server-production/14.pack +0 -0
  15. package/.next/cache/webpack/server-production/index.pack +0 -0
  16. package/.next/cache/webpack/server-production/index.pack.old +0 -0
  17. package/.next/server/app/_not-found.html +1 -1
  18. package/.next/server/app/_not-found.rsc +1 -1
  19. package/.next/server/app/index.html +1 -1
  20. package/.next/server/app/index.rsc +1 -1
  21. package/.next/server/pages/404.html +1 -1
  22. package/.next/server/pages/500.html +1 -1
  23. package/.next/trace +2 -2
  24. package/package.json +2 -2
  25. package/src/app/actions/audit.actions.ts +71 -0
  26. package/src/app/actions/graph.actions.ts +25 -0
  27. package/src/app/favicon.ico +0 -0
  28. package/src/app/globals.css +77 -0
  29. package/src/app/layout.tsx +30 -0
  30. package/src/app/page.tsx +5 -0
  31. package/src/app/utils/buildGraph.ts +119 -0
  32. package/src/app/utils/getParsedFileStructure.ts +225 -0
  33. package/src/app/utils/markCyclicPackages.ts +275 -0
  34. package/src/app/utils/parser/cpp/extractCppPackageFromImport.ts +18 -0
  35. package/src/app/utils/parser/cpp/parseCppFile.ts +150 -0
  36. package/src/app/utils/parser/delphi/extractPackageFromImport.ts +21 -0
  37. package/src/app/utils/parser/delphi/parseFile.ts +179 -0
  38. package/src/app/utils/parser/java/extractJavaPackageFromImport.ts +39 -0
  39. package/src/app/utils/parser/java/findEntryPoint.ts +24 -0
  40. package/src/app/utils/parser/java/getIntrinsicPackagesRecursive.ts +33 -0
  41. package/src/app/utils/parser/java/parseJavaFile.ts +114 -0
  42. package/src/app/utils/parser/kotlin/extractPackageFromImport.ts +19 -0
  43. package/src/app/utils/parser/kotlin/parseFile.ts +147 -0
  44. package/src/app/utils/parser/python/extractPythonPackageFromImport.ts +18 -0
  45. package/src/app/utils/parser/python/parseFile.ts +171 -0
  46. package/src/app/utils/parser/typescript/extractTypeScriptPackageFromImport.ts +18 -0
  47. package/src/app/utils/parser/typescript/parseFile.ts +130 -0
  48. package/src/components/Breadcrumb.tsx +34 -0
  49. package/src/components/Cytoscape.tsx +23 -0
  50. package/src/components/Header.tsx +28 -0
  51. package/src/components/Loader.tsx +10 -0
  52. package/src/components/Setting.tsx +17 -0
  53. package/src/components/Settings.tsx +189 -0
  54. package/src/components/Switch.tsx +31 -0
  55. package/src/components/ThemeToggle.tsx +25 -0
  56. package/src/components/ZoomInput.tsx +94 -0
  57. package/src/components/useCytoscape.ts +343 -0
  58. package/src/contexts/SettingsContext.tsx +88 -0
  59. package/src/i18n/en.ts +27 -0
  60. package/src/i18n/i18n.ts +12 -0
  61. package/src/layouts/breadthfirst/layout.ts +30 -0
  62. package/src/layouts/breadthfirst/style.ts +8 -0
  63. package/src/layouts/circle/layout.ts +11 -0
  64. package/src/layouts/circle/style.ts +18 -0
  65. package/src/layouts/concentric/layout.ts +10 -0
  66. package/src/layouts/concentric/style.ts +16 -0
  67. package/src/layouts/constants.ts +17 -0
  68. package/src/layouts/elk/layout.ts +55 -0
  69. package/src/layouts/elk/style.ts +14 -0
  70. package/src/layouts/getLayoutStyle.ts +19 -0
  71. package/src/layouts/getWeightBuckets.ts +58 -0
  72. package/src/layouts/grid/layout.ts +11 -0
  73. package/src/layouts/grid/style.ts +20 -0
  74. package/src/layouts/index.ts +14 -0
  75. package/src/layouts/style.ts +191 -0
  76. package/src/screens/home/Home.tsx +48 -0
  77. package/src/shared/constants/index.ts +7 -0
  78. package/src/shared/types/index.ts +68 -0
  79. package/src/shared/utils/detectLanguage.ts +255 -0
  80. package/src/shared/utils/getJsonAsync.ts +13 -0
  81. package/src/shared/utils/getProjectName.ts +3 -0
  82. package/src/shared/utils/parseEnv.ts +91 -0
  83. package/src/shared/utils/parseProjectPath.ts +8 -0
  84. package/src/store/useLocalStorage.ts +29 -0
  85. package/src/utils/filter/filterByPackagePrefix.ts +23 -0
  86. package/src/utils/filter/filterEmptyPackages.ts +36 -0
  87. package/src/utils/filter/filterSubPackagesFromDepth.ts +170 -0
  88. package/src/utils/filter/filterVendorPackages.ts +17 -0
  89. package/src/utils/filter/toggleCompoundNodes.ts +40 -0
  90. package/src/utils/hasChildren.ts +7 -0
  91. /package/.next/static/{oEEpBF0R0rk0kWAKiKYbp → cGbLRcmdy7k8HGus5vhzm}/_buildManifest.js +0 -0
  92. /package/.next/static/{oEEpBF0R0rk0kWAKiKYbp → cGbLRcmdy7k8HGus5vhzm}/_ssgManifest.js +0 -0
@@ -0,0 +1,3 @@
1
+ import { parseProjectPath } from '@/shared/utils/parseProjectPath';
2
+
3
+ export const getProjectName = () => parseProjectPath().split('/').pop() || '{unknown}';
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Parses an environment variable value into the appropriate type.
3
+ * @example 'true' => true
4
+ * @example 'false' => false
5
+ * @example '123' => 123
6
+ * @example 'text' => 'text'
7
+ */
8
+ export const parseEnv = (name: string, value: string | undefined) => {
9
+ if (!value) return undefined;
10
+ if (value === 'true') return true;
11
+ if (value === 'false') return false;
12
+ if (!isNaN(Number(value))) return Number(value);
13
+ return value;
14
+ };
15
+
16
+ /**
17
+ * Determines the active Cytoscape layout
18
+ * @default 'concentric'
19
+ * @todo Create enum for the layouts
20
+ * @returns 'grid' | 'circle' | 'elk' | 'concentric'
21
+ */
22
+ export const getCytoscapeLayout = () => {
23
+ const env = parseEnv('NEXT_PUBLIC_SETTINGS_LAYOUT', process.env.NEXT_PUBLIC_SETTINGS_LAYOUT);
24
+ return env === 'grid'
25
+ ? 'grid'
26
+ : env === 'circle'
27
+ ? 'circle'
28
+ : env === 'elk'
29
+ ? 'elk'
30
+ : 'concentric';
31
+ };
32
+
33
+ /**
34
+ * Determines the active Cytoscape layout spacing.
35
+ * @returns number
36
+ * @default 1
37
+ * @min 0.1
38
+ * @max 1
39
+ */
40
+ export const getCytoscapeLayoutSpacing = () => {
41
+ const env = parseEnv(
42
+ 'NEXT_PUBLIC_SETTINGS_LAYOUT_SPACING',
43
+ process.env.NEXT_PUBLIC_SETTINGS_LAYOUT_SPACING
44
+ );
45
+
46
+ return isNaN(Number(env)) ? 1 : Number(env);
47
+ };
48
+
49
+ /**
50
+ * Determines whether to show compound nodes.
51
+ * @default true
52
+ * @returns boolean
53
+ */
54
+ export const getShowCompoundNodes = () => {
55
+ const env = parseEnv(
56
+ 'NEXT_PUBLIC_SETTINGS_SHOW_COMPOUNDNODES',
57
+ process.env.NEXT_PUBLIC_SETTINGS_SHOW_COMPOUNDNODES
58
+ );
59
+
60
+ return env === false ? false : true;
61
+ };
62
+
63
+ /**
64
+ * Determines whether to show vendor packages.
65
+ * @default false
66
+ * @returns boolean
67
+ */
68
+ export const getShowVendorPackages = () => {
69
+ const env = parseEnv(
70
+ 'NEXT_PUBLIC_SETTINGS_SHOW_VENDORPACKAGES',
71
+ process.env.NEXT_PUBLIC_SETTINGS_SHOW_VENDORPACKAGES
72
+ );
73
+
74
+ return env === true ? true : false;
75
+ };
76
+
77
+ /**
78
+ * Determines how many subpackage levels to show.
79
+ * @default 1
80
+ * @min 1
81
+ * @max {automatic based on package depth}
82
+ * @returns number
83
+ */
84
+ export const getSubPackageDepth = () => {
85
+ const env = parseEnv(
86
+ 'NEXT_PUBLIC_SETTINGS_SUBPACKAGE_DEPTH',
87
+ process.env.NEXT_PUBLIC_SETTINGS_SUBPACKAGE_DEPTH
88
+ );
89
+
90
+ return typeof env === 'string' && env.length ? parseInt(env) : 1;
91
+ };
@@ -0,0 +1,8 @@
1
+ import { toPosix } from '@/shared/utils/toPosix';
2
+
3
+ export const parseProjectPath = () => {
4
+ const projectPath = process.env.NEXT_PUBLIC_PROJECT_PATH;
5
+ if (!projectPath) throw new Error('Missing ENV: NEXT_PUBLIC_PROJECT_PATH');
6
+
7
+ return toPosix(projectPath);
8
+ };
@@ -0,0 +1,29 @@
1
+ import { useState } from 'react';
2
+
3
+ export function useLocalStorage<T>(key: string, initialValue: T) {
4
+ const [storedValue, setStoredValue] = useState<T>(() => {
5
+ if (typeof window === 'undefined') return initialValue;
6
+
7
+ try {
8
+ const item = window.localStorage.getItem(key);
9
+ return item ? (JSON.parse(item) as T) : initialValue;
10
+ } catch (error) {
11
+ console.warn(`Error reading localStorage key "${key}":`, error);
12
+ return initialValue;
13
+ }
14
+ });
15
+
16
+ const setValue = (value: T | ((val: T) => T)) => {
17
+ try {
18
+ const valueToStore = value instanceof Function ? value(storedValue) : value;
19
+ setStoredValue(valueToStore);
20
+ if (typeof window !== 'undefined') {
21
+ window.localStorage.setItem(key, JSON.stringify(valueToStore));
22
+ }
23
+ } catch (error) {
24
+ console.warn(`Error setting localStorage key "${key}":`, error);
25
+ }
26
+ };
27
+
28
+ return [storedValue, setValue] as const;
29
+ }
@@ -0,0 +1,23 @@
1
+ export function filterByPackagePrefix(
2
+ allElements: cytoscape.ElementsDefinition,
3
+ packagePrefix: string
4
+ ): cytoscape.ElementsDefinition {
5
+ // PackageView entrypoint, no prefix
6
+ if (!packagePrefix) return allElements;
7
+
8
+ // Active filtering (subpackage view)
9
+ const pkgPrefix = packagePrefix.endsWith('.') ? packagePrefix : packagePrefix + '.';
10
+ const allowedNodes = allElements.nodes.filter(node => {
11
+ return (node.data.id as string).startsWith(pkgPrefix);
12
+ });
13
+
14
+ const allowedNodeIds = new Set(allowedNodes.map(node => node.data.id));
15
+ const allowedEdges = allElements.edges.filter(
16
+ edge => allowedNodeIds.has(edge.data.source) && allowedNodeIds.has(edge.data.target)
17
+ );
18
+
19
+ return {
20
+ nodes: allowedNodes,
21
+ edges: allowedEdges,
22
+ };
23
+ }
@@ -0,0 +1,36 @@
1
+ import { ElementsDefinition } from 'cytoscape';
2
+
3
+ /**
4
+ * Returns root package if only one inside, or '' (root folder)
5
+ */
6
+ function getRootPackage(elements: ElementsDefinition) {
7
+ const rootPackages = elements.nodes.filter(n => {
8
+ return !n.data.id?.includes('.');
9
+ });
10
+ return rootPackages.length > 1 ? '' : (rootPackages[0].data.id as string);
11
+ }
12
+
13
+ /**
14
+ * Filter empty packages from graph (skip to next interesting package)
15
+ */
16
+ export function filterEmptyPackages(currentPackage: string, elements: ElementsDefinition): string {
17
+ const nodes = elements.nodes.map(n => n.data.id!); // Non-null assertion
18
+
19
+ if (!currentPackage) return getRootPackage(elements);
20
+
21
+ while (true) {
22
+ const childPackages = nodes.filter(
23
+ id =>
24
+ id.startsWith(currentPackage + '.') &&
25
+ id.split('.').length === currentPackage.split('.').length + 1
26
+ );
27
+
28
+ if (childPackages.length === 1) {
29
+ currentPackage = childPackages[0];
30
+ } else {
31
+ break;
32
+ }
33
+ }
34
+
35
+ return currentPackage;
36
+ }
@@ -0,0 +1,170 @@
1
+ import type { EdgeDefinition, ElementsDefinition } from 'cytoscape';
2
+
3
+ /**
4
+ * Compute the minimal set of "root" packages that have no ancestor in the list.
5
+ * Example: ['x.y', 'x.y.z', 'a', 'a.c.x'] => ['a', 'x.y']
6
+ */
7
+ function findRoots(packages: string[]): string[] {
8
+ const sorted = [...packages].sort(); // parents before children (lexicographically close enough for dotted paths)
9
+ const roots: string[] = [];
10
+
11
+ for (const pkg of sorted) {
12
+ if (!roots.some(parent => pkg.startsWith(parent + '.'))) {
13
+ roots.push(pkg);
14
+ }
15
+ }
16
+ return roots;
17
+ }
18
+
19
+ /**
20
+ * Ascend from a package to its nearest ancestor that is in the `roots` set.
21
+ */
22
+ function nearestRoot(pkg: string, roots: Set<string>): string {
23
+ if (roots.has(pkg)) return pkg;
24
+ const parts = pkg.split('.');
25
+ while (parts.length > 1) {
26
+ parts.pop();
27
+ const ancestor = parts.join('.');
28
+ if (roots.has(ancestor)) return ancestor;
29
+ }
30
+ // Fallback: if nothing matched (shouldn't happen if roots are computed from the same list)
31
+ return pkg;
32
+ }
33
+
34
+ /**
35
+ * Given a root and a leaf pkg, return the visible ancestor at `maxDepth`
36
+ * under the root, preferring the deepest existing package <= target depth.
37
+ *
38
+ * Example:
39
+ * root = 'a', pkg = 'a.b.c.d', maxDepth=2 → tries 'a.b' (if exists), else falls back to 'a'
40
+ */
41
+ function visibleAncestorForDepth(
42
+ root: string,
43
+ pkg: string,
44
+ maxDepth: number,
45
+ existing: Set<string>
46
+ ): string {
47
+ // Depth is counted as: root = depth 1; root.child = depth 2; etc.
48
+ if (maxDepth <= 1) return root;
49
+
50
+ const rootDepth = root.split('.').length;
51
+ const pkgParts = pkg.split('.');
52
+ const desiredDepth = Math.min(rootDepth + (maxDepth - 1), pkgParts.length);
53
+
54
+ // Walk downwards from desiredDepth to rootDepth until we find an existing node
55
+ for (let d = desiredDepth; d >= rootDepth; d--) {
56
+ const candidate = pkgParts.slice(0, d).join('.');
57
+ if (existing.has(candidate)) return candidate;
58
+ }
59
+ return root; // fallback
60
+ }
61
+
62
+ /**
63
+ * Global maximum depth across all roots.
64
+ * Depth is counted relative to the nearest root: root=1, root.child=2, etc.
65
+ */
66
+ export function getMaxDepth(elements: ElementsDefinition): number {
67
+ const pkgs = elements.nodes.map(n => String(n.data.id));
68
+ if (pkgs.length === 0) return 0;
69
+
70
+ const roots = findRoots(pkgs);
71
+ const rootSet = new Set(roots);
72
+
73
+ let maxDepth = 1;
74
+ for (const pkg of pkgs) {
75
+ const root = nearestRoot(pkg, rootSet);
76
+ const depth = pkg.split('.').length - root.split('.').length + 1; // root=1
77
+ if (depth > maxDepth) maxDepth = depth;
78
+ }
79
+ return maxDepth;
80
+ }
81
+
82
+ /**
83
+ * Maximum depth per root, e.g. { 'a': 3, 'x.y': 2 }.
84
+ */
85
+ export function getMaxDepthByRoot(elements: ElementsDefinition): Record<string, number> {
86
+ const pkgs = elements.nodes.map(n => String(n.data.id));
87
+ const roots = findRoots(pkgs);
88
+ const rootSet = new Set(roots);
89
+
90
+ const result: Record<string, number> = Object.fromEntries(roots.map(r => [r, 1]));
91
+ for (const pkg of pkgs) {
92
+ const root = nearestRoot(pkg, rootSet);
93
+ const depth = pkg.split('.').length - root.split('.').length + 1;
94
+ if (depth > result[root]) result[root] = depth;
95
+ }
96
+ return result;
97
+ }
98
+
99
+ /**
100
+ * Filters sub-packages so only the desired number of levels under each root are shown.
101
+ *
102
+ * @param elements - Cytoscape elements
103
+ * @param allowSelfLoops - Keep self loops after lifting/aggregation
104
+ * @param maxDepth - Number of levels to keep under each root (1 = roots only, 2 = +children, etc.)
105
+ */
106
+ export function filterSubPackagesByDepth(
107
+ elements: ElementsDefinition,
108
+ allowSelfLoops: boolean = false,
109
+ maxDepth: number = 1
110
+ ): ElementsDefinition {
111
+ const allPackages = elements.nodes.map(n => n.data.id as string);
112
+ const existingSet = new Set(allPackages);
113
+
114
+ // 1) Identify minimal roots (no ancestor present)
115
+ const roots = findRoots(allPackages);
116
+ const rootSet = new Set(roots);
117
+
118
+ // 2) Map every package to its visible ancestor according to maxDepth
119
+ const pkgToVisible = new Map<string, string>();
120
+ for (const pkg of allPackages) {
121
+ const root = nearestRoot(pkg, rootSet);
122
+ const visible = visibleAncestorForDepth(root, pkg, maxDepth, existingSet);
123
+ pkgToVisible.set(pkg, visible);
124
+ }
125
+
126
+ // 3) Visible nodes = unique set of chosen ancestors
127
+ const visibleNodeIds = new Set<string>(pkgToVisible.values());
128
+ const filteredNodes = elements.nodes.filter(n => visibleNodeIds.has(n.data.id as string));
129
+
130
+ // 4) Lift & aggregate edges to visible ancestors (sum weights)
131
+ const edgeMap = new Map<string, EdgeDefinition>();
132
+
133
+ for (const e of elements.edges) {
134
+ const rawSource = e.data.source as string;
135
+ const rawTarget = e.data.target as string;
136
+
137
+ const liftedSource = pkgToVisible.get(rawSource) ?? rawSource;
138
+ const liftedTarget = pkgToVisible.get(rawTarget) ?? rawTarget;
139
+
140
+ if (!visibleNodeIds.has(liftedSource) || !visibleNodeIds.has(liftedTarget)) {
141
+ continue;
142
+ }
143
+ if (!allowSelfLoops && liftedSource === liftedTarget) {
144
+ continue;
145
+ }
146
+
147
+ const key = `${liftedSource}->${liftedTarget}`;
148
+ if (!edgeMap.has(key)) {
149
+ // clone with lifted endpoints; keep your metadata; (optionally set a stable id)
150
+ edgeMap.set(key, {
151
+ ...e,
152
+ data: {
153
+ ...e.data,
154
+ // id: key, // uncomment if you want stable, deduped edge ids
155
+ source: liftedSource,
156
+ target: liftedTarget,
157
+ // initialize aggregated weight
158
+ weight: (e.data.weight as number | undefined) ?? 1,
159
+ },
160
+ });
161
+ } else {
162
+ const existing = edgeMap.get(key)!;
163
+ const add = (e.data.weight as number | undefined) ?? 1;
164
+ existing.data.weight = ((existing.data.weight as number | undefined) ?? 0) + add;
165
+ }
166
+ }
167
+
168
+ const filteredEdges = Array.from(edgeMap.values());
169
+ return { nodes: filteredNodes, edges: filteredEdges };
170
+ }
@@ -0,0 +1,17 @@
1
+ import type { ElementsDefinition } from 'cytoscape';
2
+
3
+ /**
4
+ * Filters vendor packages by known package prefix
5
+ */
6
+ export function filterVendorPackages(elements: ElementsDefinition): ElementsDefinition {
7
+ const filteredNodes = elements.nodes.filter(node => !!node.data.isIntrinsic);
8
+ const allowedNodeIds = new Set(filteredNodes.map(node => node.data.id));
9
+ const filteredEdges = elements.edges.filter(
10
+ edge => allowedNodeIds.has(edge.data.source) && allowedNodeIds.has(edge.data.target)
11
+ );
12
+
13
+ return {
14
+ nodes: filteredNodes,
15
+ edges: filteredEdges,
16
+ };
17
+ }
@@ -0,0 +1,40 @@
1
+ import { ElementsDefinition } from 'cytoscape';
2
+
3
+ /**
4
+ * Toggles display of compound nodes ('parent' attribute) in the node
5
+ * @param elements - Cytoscape elements (nodes and edges)
6
+ * @param show - Whether to show compound nodes
7
+ * @param currentPackage - Current package path to adjust node names when showing compound nodes
8
+ * @returns Updated Cytoscape elements with compound nodes toggled
9
+ */
10
+ export function toggleCompoundNodes(
11
+ { nodes, edges }: ElementsDefinition,
12
+ show: boolean,
13
+ currentPackage: string
14
+ ): ElementsDefinition {
15
+ const updatedNodes = nodes.map(node => {
16
+ if (!node.data.idInactive) node.data.idInactive = node.data.id;
17
+ // ACTIVE Compound Nodes
18
+ if (show && node.data.parentInactive) {
19
+ node.data.parent = node.data.parentInactive;
20
+ node.data.parentInactive = undefined;
21
+ } else if (!show && node.data.parent) {
22
+ node.data.parentInactive = node.data.parent;
23
+ node.data.parent = undefined;
24
+ }
25
+ return node;
26
+ });
27
+
28
+ const labelledNodes = updatedNodes.map(node => {
29
+ if (show) {
30
+ node.data.name = node.data.idInactive.split('.').pop();
31
+ } else {
32
+ node.data.name = node.data.idInactive.slice(
33
+ currentPackage.length ? currentPackage.length + 1 : 0
34
+ );
35
+ }
36
+ return node;
37
+ });
38
+
39
+ return { nodes: labelledNodes, edges };
40
+ }
@@ -0,0 +1,7 @@
1
+ import type { NodeDataDefinition, NodeDefinition } from 'cytoscape';
2
+
3
+ /**
4
+ * Checks if a node (package) has children based on id prefix.
5
+ */
6
+ export const hasChildren = (node: NodeDefinition, nodes: NodeDataDefinition[]) =>
7
+ nodes.some(n => n.data.id?.startsWith(`${node.data.id}.`));