@zoldia/omnigraph 1.0.0 → 1.1.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 +28 -14
- package/dist/cli.js +396 -1
- package/package.json +7 -3
- package/ui/assets/index-B4R9LC7F.js +23 -0
- package/ui/gif.worker.js +3 -0
- package/ui/index.html +1 -1
- package/ui/assets/index-DKa11qnN.js +0 -11
package/README.md
CHANGED
|
@@ -8,10 +8,11 @@ OmniGraph is a free, local developer tool that statically analyzes full-stack mo
|
|
|
8
8
|
|
|
9
9
|
| Language | Extensions | Framework Detection |
|
|
10
10
|
|----------|-----------|-------------------|
|
|
11
|
-
| **TypeScript** | `.ts`, `.tsx` | NestJS (`@Controller`, `@Injectable`, `@Module`) |
|
|
11
|
+
| **TypeScript** | `.ts`, `.tsx` | NestJS (`@Controller`, `@Injectable`, `@Module`), Next.js (App Router, Pages Router) |
|
|
12
12
|
| **JavaScript** | `.js`, `.jsx` | CommonJS and ES module imports |
|
|
13
13
|
| **Python** | `.py` | FastAPI (`@router.get`, `@app.post`), Flask (`@app.route`), Django (Views, Models) |
|
|
14
14
|
| **PHP** | `.php` | Laravel (Controllers, Models, Middleware, Route definitions) |
|
|
15
|
+
| **Markdown** | `.md`, `.mdx` | Obsidian wiki-links (`[[Page]]`), embeds (`![[Page]]`), frontmatter tags/aliases |
|
|
15
16
|
|
|
16
17
|
## Quick Start
|
|
17
18
|
|
|
@@ -44,28 +45,38 @@ omnigraph --path <repo-path> --port 4000 # Optional: custom port (default 3000)
|
|
|
44
45
|
## Features
|
|
45
46
|
|
|
46
47
|
### Multi-Language Dependency Graph
|
|
47
|
-
Point OmniGraph at any project containing TypeScript, JavaScript, Python, or
|
|
48
|
+
Point OmniGraph at any project containing TypeScript, JavaScript, Python, PHP, or Markdown files. It recursively walks the directory, respects `.gitignore`, and builds a dependency graph from import/require statements and wiki-links.
|
|
48
49
|
|
|
49
50
|
### Framework-Aware Parsing
|
|
50
51
|
OmniGraph doesn't just find imports — it understands framework patterns:
|
|
51
52
|
- **NestJS**: Detects `@Controller`, `@Injectable`, `@Module` decorators with route metadata
|
|
53
|
+
- **Next.js**: Detects App Router (`route.ts`, `page.tsx`, `layout.tsx`) and Pages Router (`pages/api/`)
|
|
52
54
|
- **FastAPI/Flask**: Detects route decorators (`@router.get("/users")`) with HTTP methods and paths
|
|
53
55
|
- **Django**: Detects class-based views (`APIView`, `ViewSet`) and models
|
|
54
56
|
- **Laravel**: Detects controllers, models, middleware, and `Route::get()` definitions
|
|
57
|
+
- **Obsidian/Markdown**: Detects wiki-links (`[[Page]]`), embeds (`![[Page]]`), YAML frontmatter (tags, aliases), and classifies MOC/daily/readme note types
|
|
55
58
|
|
|
56
59
|
### Interactive Visualization
|
|
57
60
|
- **5 Layout Presets**: Directory (grouped by folder), Hierarchical, Force-Directed, Grid, Mind Map (LR/RL)
|
|
58
61
|
- **Live Force Simulation**: In force-directed mode, dragging a node causes nearby nodes to push and pull reactively via d3-force physics
|
|
59
|
-
- **Search & Filter**: Search nodes by name, filter by type with color-coded toggle chips
|
|
62
|
+
- **Search & Filter with BFS Expansion**: Search nodes by name, filter by type with color-coded toggle chips. Hide or dim non-matching nodes. Connected nodes expand via BFS depth slider to reveal full data flow paths.
|
|
63
|
+
- **Hub-Centric Compaction**: After filtering, compact visible nodes around the most-connected hub node(s) using d3-force. Single hub stays pinned; multiple hubs meet at their average position.
|
|
60
64
|
- **Node Inspector**: Click any node to see its file path, type, route metadata, and ID in the sidebar
|
|
61
|
-
- **Color-Coded Types**: Each node type has a distinct color — controllers (red), injectables (blue), modules (orange), Python files (blue), FastAPI routes (teal), Laravel controllers (red), and more
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
-
|
|
68
|
-
-
|
|
65
|
+
- **Color-Coded Types**: Each node type has a distinct color — controllers (red), injectables (blue), modules (orange), Python files (blue), FastAPI routes (teal), Laravel controllers (red), markdown (purple), and more
|
|
66
|
+
- **Clickable Minimap**: Zoom and pan directly on the minimap for faster navigation
|
|
67
|
+
|
|
68
|
+
### Export
|
|
69
|
+
- **PNG** — 2x resolution raster image
|
|
70
|
+
- **SVG** — Scalable vector graphic
|
|
71
|
+
- **JSON** — Raw OmniGraph data
|
|
72
|
+
- **GIF** — 1-second animated GIF (30fps) showing edge flow direction with a progress overlay
|
|
73
|
+
|
|
74
|
+
### Sidebar Tabs
|
|
75
|
+
The right sidebar has four tabs:
|
|
76
|
+
- **Graph** — Layout selector, search/filter with depth slider, type chips, node inspector, export dropdown, compact button
|
|
77
|
+
- **API** — Postman-style API debugger (auto-fills from cross-network edges)
|
|
78
|
+
- **Trace** — Step-through flow tracer with Back/Next navigation and animated highlighting
|
|
79
|
+
- **Settings** — Configurable edge labels (show/hide per type, color, font size), graph options (minimap, animations), search defaults, with per-category reset and localStorage persistence
|
|
69
80
|
|
|
70
81
|
## Technology Stack
|
|
71
82
|
|
|
@@ -77,9 +88,11 @@ All controls live in a clean right sidebar:
|
|
|
77
88
|
| TypeScript/JS Parser | `@typescript-eslint/typescript-estree` |
|
|
78
89
|
| Python Parser | Regex-based AST extraction |
|
|
79
90
|
| PHP Parser | Regex-based AST extraction |
|
|
91
|
+
| Markdown Parser | Regex-based wiki-link/embed/frontmatter extraction |
|
|
80
92
|
| Frontend | React 18 + Vite |
|
|
81
93
|
| Graph Engine | React Flow |
|
|
82
|
-
| Layout Engines | dagre (hierarchical/mind map), d3-force (force-directed) |
|
|
94
|
+
| Layout Engines | dagre (hierarchical/mind map), d3-force (force-directed, compaction) |
|
|
95
|
+
| GIF Export | gif.js (web worker encoding) |
|
|
83
96
|
| Testing | Vitest |
|
|
84
97
|
|
|
85
98
|
## Architecture
|
|
@@ -152,7 +165,7 @@ All parsers produce a standardized graph format regardless of source language:
|
|
|
152
165
|
## Running Tests
|
|
153
166
|
|
|
154
167
|
```bash
|
|
155
|
-
npx vitest run # Run all tests (
|
|
168
|
+
npx vitest run # Run all tests (132 tests across 9 files)
|
|
156
169
|
npx vitest --watch # Watch mode
|
|
157
170
|
```
|
|
158
171
|
|
|
@@ -169,9 +182,9 @@ Contributions are welcome! Here's how to get started:
|
|
|
169
182
|
### Good First Issues
|
|
170
183
|
|
|
171
184
|
- Add a new language parser (Go, Rust, Java, C#, Ruby)
|
|
172
|
-
- Add export functionality (SVG, PNG, JSON)
|
|
173
185
|
- Improve import resolution for edge cases (barrel exports, dynamic imports)
|
|
174
186
|
- Add dark/light theme toggle
|
|
187
|
+
- WebSocket connection tracing
|
|
175
188
|
|
|
176
189
|
## Project Documentation
|
|
177
190
|
|
|
@@ -181,6 +194,7 @@ Contributions are welcome! Here's how to get started:
|
|
|
181
194
|
| [SAD](docs/SAD.md) | Software architecture, data flow, and design decisions |
|
|
182
195
|
| [ADR-001](docs/adr/ADR-001-parsing-engine.md) | Why typescript-estree for Phase 1 |
|
|
183
196
|
| [ADR-002](docs/adr/ADR-002-phase2-multi-language-parsing.md) | Why regex-based parsing for Phase 2 Python/PHP |
|
|
197
|
+
| [ADR-003](docs/adr/ADR-003-markdown-obsidian-parser.md) | Markdown/Obsidian wiki-link parser design |
|
|
184
198
|
| [API Spec](docs/API-SPEC.md) | HTTP endpoint and CLI interface documentation |
|
|
185
199
|
|
|
186
200
|
## License
|
package/dist/cli.js
CHANGED
|
@@ -259851,6 +259851,89 @@ var require_typescript_parser = __commonJS({
|
|
|
259851
259851
|
}
|
|
259852
259852
|
return null;
|
|
259853
259853
|
}
|
|
259854
|
+
var NEXTJS_HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
259855
|
+
function detectNextJSAppRoute(filePath, exportedNames) {
|
|
259856
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
259857
|
+
const basename = path2.basename(normalized, path2.extname(normalized));
|
|
259858
|
+
if (basename !== "route")
|
|
259859
|
+
return null;
|
|
259860
|
+
const appMatch = normalized.match(/(?:^|\/)(src\/)?app\/(.*?)\/route\.\w+$/);
|
|
259861
|
+
if (!appMatch)
|
|
259862
|
+
return null;
|
|
259863
|
+
const routeDir = appMatch[2];
|
|
259864
|
+
const routePath = "/" + routeDir.split("/").map((segment) => {
|
|
259865
|
+
if (/^\[\[?\.\.\.\w+\]?\]$/.test(segment)) {
|
|
259866
|
+
const name = segment.replace(/[\[\]\.]/g, "");
|
|
259867
|
+
return `:${name}*`;
|
|
259868
|
+
}
|
|
259869
|
+
if (/^\[\w+\]$/.test(segment)) {
|
|
259870
|
+
return ":" + segment.slice(1, -1);
|
|
259871
|
+
}
|
|
259872
|
+
return segment;
|
|
259873
|
+
}).join("/");
|
|
259874
|
+
const methods = exportedNames.filter((n) => NEXTJS_HTTP_METHODS.includes(n));
|
|
259875
|
+
if (methods.length === 0) {
|
|
259876
|
+
return { nodeType: "nextjs-api-route", route: routePath };
|
|
259877
|
+
}
|
|
259878
|
+
const routeEntries = methods.map((m) => `${m} ${routePath}`).join(", ");
|
|
259879
|
+
return { nodeType: "nextjs-api-route", route: routeEntries };
|
|
259880
|
+
}
|
|
259881
|
+
function detectNextJSPagesApiRoute(filePath, hasDefaultExport) {
|
|
259882
|
+
if (!hasDefaultExport)
|
|
259883
|
+
return null;
|
|
259884
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
259885
|
+
const basename = path2.basename(normalized, path2.extname(normalized));
|
|
259886
|
+
const pagesMatch = normalized.match(/(?:^|\/)(src\/)?pages\/api\/(.*?)(\.\w+)$/);
|
|
259887
|
+
if (!pagesMatch)
|
|
259888
|
+
return null;
|
|
259889
|
+
let routeSegments = pagesMatch[2];
|
|
259890
|
+
if (basename === "index") {
|
|
259891
|
+
routeSegments = routeSegments.replace(/\/?index$/, "");
|
|
259892
|
+
}
|
|
259893
|
+
const segments = routeSegments.split("/").filter(Boolean).map((segment) => {
|
|
259894
|
+
if (/^\[\[?\.\.\.\w+\]?\]$/.test(segment)) {
|
|
259895
|
+
const name = segment.replace(/[\[\]\.]/g, "");
|
|
259896
|
+
return `:${name}*`;
|
|
259897
|
+
}
|
|
259898
|
+
if (/^\[\w+\]$/.test(segment)) {
|
|
259899
|
+
return ":" + segment.slice(1, -1);
|
|
259900
|
+
}
|
|
259901
|
+
return segment;
|
|
259902
|
+
});
|
|
259903
|
+
const routePath = segments.length > 0 ? "/api/" + segments.join("/") : "/api";
|
|
259904
|
+
return { nodeType: "nextjs-api-route", route: routePath };
|
|
259905
|
+
}
|
|
259906
|
+
function detectNextJSPage(filePath) {
|
|
259907
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
259908
|
+
const basename = path2.basename(normalized, path2.extname(normalized));
|
|
259909
|
+
if (basename !== "page")
|
|
259910
|
+
return null;
|
|
259911
|
+
const appMatch = normalized.match(/(?:^|\/)(src\/)?app\/(.*?)\/page\.\w+$/);
|
|
259912
|
+
if (!appMatch)
|
|
259913
|
+
return null;
|
|
259914
|
+
const routeDir = appMatch[2];
|
|
259915
|
+
const routePath = "/" + routeDir.split("/").map((segment) => {
|
|
259916
|
+
if (/^\[\[?\.\.\.\w+\]?\]$/.test(segment)) {
|
|
259917
|
+
const name = segment.replace(/[\[\]\.]/g, "");
|
|
259918
|
+
return `:${name}*`;
|
|
259919
|
+
}
|
|
259920
|
+
if (/^\[\w+\]$/.test(segment)) {
|
|
259921
|
+
return ":" + segment.slice(1, -1);
|
|
259922
|
+
}
|
|
259923
|
+
return segment;
|
|
259924
|
+
}).join("/");
|
|
259925
|
+
return { nodeType: "nextjs-page", route: routePath };
|
|
259926
|
+
}
|
|
259927
|
+
function detectNextJSLayout(filePath) {
|
|
259928
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
259929
|
+
const basename = path2.basename(normalized, path2.extname(normalized));
|
|
259930
|
+
if (basename !== "layout")
|
|
259931
|
+
return null;
|
|
259932
|
+
const appMatch = normalized.match(/(?:^|\/)(src\/)?app\/(.+\/)?layout\.\w+$/);
|
|
259933
|
+
if (!appMatch)
|
|
259934
|
+
return null;
|
|
259935
|
+
return { nodeType: "nextjs-layout", route: "" };
|
|
259936
|
+
}
|
|
259854
259937
|
var RESOLVE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"];
|
|
259855
259938
|
function resolveImport(fromFile, importPath) {
|
|
259856
259939
|
const base = path2.normalize(path2.join(path2.dirname(fromFile), importPath));
|
|
@@ -259888,6 +259971,8 @@ var require_typescript_parser = __commonJS({
|
|
|
259888
259971
|
const isJS = /\.(js|jsx)$/.test(filePath);
|
|
259889
259972
|
let nodeType = isJS ? "javascript-file" : "typescript-file";
|
|
259890
259973
|
let route = "";
|
|
259974
|
+
const exportedNames = [];
|
|
259975
|
+
let hasDefaultExport = false;
|
|
259891
259976
|
for (const stmt of ast.body) {
|
|
259892
259977
|
if (stmt.type === "ImportDeclaration") {
|
|
259893
259978
|
const src = stmt.source.value;
|
|
@@ -259904,6 +259989,31 @@ var require_typescript_parser = __commonJS({
|
|
|
259904
259989
|
}
|
|
259905
259990
|
}
|
|
259906
259991
|
}
|
|
259992
|
+
if (stmt.type === "ExportNamedDeclaration") {
|
|
259993
|
+
if (stmt.declaration) {
|
|
259994
|
+
const decl = stmt.declaration;
|
|
259995
|
+
if (decl.type === "FunctionDeclaration" && decl.id && typeof decl.id.name === "string") {
|
|
259996
|
+
exportedNames.push(decl.id.name);
|
|
259997
|
+
}
|
|
259998
|
+
if (decl.type === "VariableDeclaration" && Array.isArray(decl.declarations)) {
|
|
259999
|
+
for (const d of decl.declarations) {
|
|
260000
|
+
if (d.id && typeof d.id.name === "string") {
|
|
260001
|
+
exportedNames.push(d.id.name);
|
|
260002
|
+
}
|
|
260003
|
+
}
|
|
260004
|
+
}
|
|
260005
|
+
}
|
|
260006
|
+
if (stmt.specifiers) {
|
|
260007
|
+
for (const spec of stmt.specifiers) {
|
|
260008
|
+
if (spec.exported && typeof spec.exported.name === "string") {
|
|
260009
|
+
exportedNames.push(spec.exported.name);
|
|
260010
|
+
}
|
|
260011
|
+
}
|
|
260012
|
+
}
|
|
260013
|
+
}
|
|
260014
|
+
if (stmt.type === "ExportDefaultDeclaration") {
|
|
260015
|
+
hasDefaultExport = true;
|
|
260016
|
+
}
|
|
259907
260017
|
let classNode = null;
|
|
259908
260018
|
if (stmt.type === "ExportNamedDeclaration" && stmt.declaration?.type === "ClassDeclaration") {
|
|
259909
260019
|
classNode = stmt.declaration;
|
|
@@ -259919,6 +260029,13 @@ var require_typescript_parser = __commonJS({
|
|
|
259919
260029
|
}
|
|
259920
260030
|
}
|
|
259921
260031
|
}
|
|
260032
|
+
if (nodeType === "typescript-file" || nodeType === "javascript-file") {
|
|
260033
|
+
const nextResult = detectNextJSAppRoute(filePath, exportedNames) ?? detectNextJSPagesApiRoute(filePath, hasDefaultExport) ?? detectNextJSPage(filePath) ?? detectNextJSLayout(filePath);
|
|
260034
|
+
if (nextResult) {
|
|
260035
|
+
nodeType = nextResult.nodeType;
|
|
260036
|
+
route = nextResult.route;
|
|
260037
|
+
}
|
|
260038
|
+
}
|
|
259922
260039
|
const node = {
|
|
259923
260040
|
id: fileId,
|
|
259924
260041
|
type: nodeType,
|
|
@@ -260433,6 +260550,281 @@ var require_php_parser = __commonJS({
|
|
|
260433
260550
|
}
|
|
260434
260551
|
});
|
|
260435
260552
|
|
|
260553
|
+
// packages/parsers/dist/markdown/markdown-parser.js
|
|
260554
|
+
var require_markdown_parser = __commonJS({
|
|
260555
|
+
"packages/parsers/dist/markdown/markdown-parser.js"(exports2) {
|
|
260556
|
+
"use strict";
|
|
260557
|
+
var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
|
|
260558
|
+
if (k2 === void 0) k2 = k;
|
|
260559
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
260560
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
260561
|
+
desc = { enumerable: true, get: function() {
|
|
260562
|
+
return m[k];
|
|
260563
|
+
} };
|
|
260564
|
+
}
|
|
260565
|
+
Object.defineProperty(o, k2, desc);
|
|
260566
|
+
}) : (function(o, m, k, k2) {
|
|
260567
|
+
if (k2 === void 0) k2 = k;
|
|
260568
|
+
o[k2] = m[k];
|
|
260569
|
+
}));
|
|
260570
|
+
var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? (function(o, v) {
|
|
260571
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
260572
|
+
}) : function(o, v) {
|
|
260573
|
+
o["default"] = v;
|
|
260574
|
+
});
|
|
260575
|
+
var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ (function() {
|
|
260576
|
+
var ownKeys = function(o) {
|
|
260577
|
+
ownKeys = Object.getOwnPropertyNames || function(o2) {
|
|
260578
|
+
var ar = [];
|
|
260579
|
+
for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
|
|
260580
|
+
return ar;
|
|
260581
|
+
};
|
|
260582
|
+
return ownKeys(o);
|
|
260583
|
+
};
|
|
260584
|
+
return function(mod) {
|
|
260585
|
+
if (mod && mod.__esModule) return mod;
|
|
260586
|
+
var result = {};
|
|
260587
|
+
if (mod != null) {
|
|
260588
|
+
for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
260589
|
+
}
|
|
260590
|
+
__setModuleDefault(result, mod);
|
|
260591
|
+
return result;
|
|
260592
|
+
};
|
|
260593
|
+
})();
|
|
260594
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
260595
|
+
exports2.MarkdownParser = void 0;
|
|
260596
|
+
var path2 = __importStar(require("path"));
|
|
260597
|
+
var fs = __importStar(require("fs"));
|
|
260598
|
+
var FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
260599
|
+
var YAML_TAGS_INLINE = /^tags:\s*\[([^\]]*)\]/m;
|
|
260600
|
+
var YAML_TAGS_KEY = /^tags:\s*$/m;
|
|
260601
|
+
var YAML_ALIASES = /^aliases:\s*\[([^\]]*)\]/m;
|
|
260602
|
+
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".bmp"]);
|
|
260603
|
+
var MarkdownParser = class {
|
|
260604
|
+
constructor() {
|
|
260605
|
+
this.rootDir = "";
|
|
260606
|
+
}
|
|
260607
|
+
setRootDir(dir) {
|
|
260608
|
+
this.rootDir = dir;
|
|
260609
|
+
}
|
|
260610
|
+
canHandle(filePath) {
|
|
260611
|
+
const ext = path2.extname(filePath).toLowerCase();
|
|
260612
|
+
return ext === ".md" || ext === ".mdx";
|
|
260613
|
+
}
|
|
260614
|
+
parse(filePath, source) {
|
|
260615
|
+
const nodes = [];
|
|
260616
|
+
const edges = [];
|
|
260617
|
+
const fileId = filePath.replace(/\\/g, "/");
|
|
260618
|
+
const fileName = path2.basename(filePath, path2.extname(filePath));
|
|
260619
|
+
const frontmatter = this.parseFrontmatter(source);
|
|
260620
|
+
const headings = [];
|
|
260621
|
+
let headingMatch;
|
|
260622
|
+
const headingRegex = /^(#{1,6})\s+(.+)$/gm;
|
|
260623
|
+
while ((headingMatch = headingRegex.exec(source)) !== null) {
|
|
260624
|
+
headings.push(headingMatch[2].trim());
|
|
260625
|
+
}
|
|
260626
|
+
let nodeType = "markdown-file";
|
|
260627
|
+
if (frontmatter.tags.some((t) => t.toLowerCase() === "moc" || t.toLowerCase() === "index")) {
|
|
260628
|
+
nodeType = "markdown-moc";
|
|
260629
|
+
} else if (frontmatter.tags.some((t) => t.toLowerCase() === "daily" || t.toLowerCase() === "journal")) {
|
|
260630
|
+
nodeType = "markdown-daily";
|
|
260631
|
+
} else if (filePath.toLowerCase().includes("readme")) {
|
|
260632
|
+
nodeType = "markdown-readme";
|
|
260633
|
+
}
|
|
260634
|
+
const metadata = {
|
|
260635
|
+
filePath,
|
|
260636
|
+
language: "markdown"
|
|
260637
|
+
};
|
|
260638
|
+
if (headings.length > 0) {
|
|
260639
|
+
metadata.headings = headings.slice(0, 8).join(", ");
|
|
260640
|
+
}
|
|
260641
|
+
if (frontmatter.tags.length > 0) {
|
|
260642
|
+
metadata.tags = frontmatter.tags.join(", ");
|
|
260643
|
+
}
|
|
260644
|
+
if (frontmatter.aliases.length > 0) {
|
|
260645
|
+
metadata.aliases = frontmatter.aliases.join(", ");
|
|
260646
|
+
}
|
|
260647
|
+
nodes.push({
|
|
260648
|
+
id: fileId,
|
|
260649
|
+
type: nodeType,
|
|
260650
|
+
label: fileName,
|
|
260651
|
+
metadata
|
|
260652
|
+
});
|
|
260653
|
+
const body = this.stripFrontmatterAndCode(source);
|
|
260654
|
+
const linkedTargets = /* @__PURE__ */ new Set();
|
|
260655
|
+
let match;
|
|
260656
|
+
const wikiRegex = /\[\[([^\]|#]+)(?:#[^\]|]*)?\|?[^\]]*\]\]/g;
|
|
260657
|
+
while ((match = wikiRegex.exec(body)) !== null) {
|
|
260658
|
+
const target = match[1].trim();
|
|
260659
|
+
if (!target)
|
|
260660
|
+
continue;
|
|
260661
|
+
const ext = path2.extname(target).toLowerCase();
|
|
260662
|
+
if (IMAGE_EXTENSIONS.has(ext))
|
|
260663
|
+
continue;
|
|
260664
|
+
const resolved = this.resolveWikiLink(filePath, target);
|
|
260665
|
+
if (resolved && !linkedTargets.has(resolved)) {
|
|
260666
|
+
linkedTargets.add(resolved);
|
|
260667
|
+
edges.push({
|
|
260668
|
+
id: `e-${fileId}->${resolved}`,
|
|
260669
|
+
source: fileId,
|
|
260670
|
+
target: resolved,
|
|
260671
|
+
label: "links to"
|
|
260672
|
+
});
|
|
260673
|
+
}
|
|
260674
|
+
}
|
|
260675
|
+
const embedRegex = /!\[\[([^\]|#]+)(?:#[^\]|]*)?\|?[^\]]*\]\]/g;
|
|
260676
|
+
while ((match = embedRegex.exec(body)) !== null) {
|
|
260677
|
+
const target = match[1].trim();
|
|
260678
|
+
if (!target)
|
|
260679
|
+
continue;
|
|
260680
|
+
const ext = path2.extname(target).toLowerCase();
|
|
260681
|
+
if (IMAGE_EXTENSIONS.has(ext))
|
|
260682
|
+
continue;
|
|
260683
|
+
const resolved = this.resolveWikiLink(filePath, target);
|
|
260684
|
+
if (resolved && !linkedTargets.has(resolved)) {
|
|
260685
|
+
linkedTargets.add(resolved);
|
|
260686
|
+
edges.push({
|
|
260687
|
+
id: `e-${fileId}->embed-${resolved}`,
|
|
260688
|
+
source: fileId,
|
|
260689
|
+
target: resolved,
|
|
260690
|
+
label: "embeds"
|
|
260691
|
+
});
|
|
260692
|
+
}
|
|
260693
|
+
}
|
|
260694
|
+
const mdLinkRegex = /\[(?:[^\]]*)\]\((?!https?:\/\/|mailto:|#)([^)]+\.(?:md|mdx))\)/g;
|
|
260695
|
+
while ((match = mdLinkRegex.exec(body)) !== null) {
|
|
260696
|
+
const linkPath = match[1].trim();
|
|
260697
|
+
if (!linkPath)
|
|
260698
|
+
continue;
|
|
260699
|
+
const resolved = this.resolveRelativeLink(filePath, linkPath);
|
|
260700
|
+
if (resolved && !linkedTargets.has(resolved)) {
|
|
260701
|
+
linkedTargets.add(resolved);
|
|
260702
|
+
edges.push({
|
|
260703
|
+
id: `e-${fileId}->${resolved}`,
|
|
260704
|
+
source: fileId,
|
|
260705
|
+
target: resolved,
|
|
260706
|
+
label: "links to"
|
|
260707
|
+
});
|
|
260708
|
+
}
|
|
260709
|
+
}
|
|
260710
|
+
return { nodes, edges };
|
|
260711
|
+
}
|
|
260712
|
+
// ─── Frontmatter Parsing ─────────────────────────────────────────
|
|
260713
|
+
parseFrontmatter(source) {
|
|
260714
|
+
const tags = [];
|
|
260715
|
+
const aliases = [];
|
|
260716
|
+
const fmMatch = FRONTMATTER.exec(source);
|
|
260717
|
+
if (!fmMatch)
|
|
260718
|
+
return { tags, aliases };
|
|
260719
|
+
const fmBlock = fmMatch[1];
|
|
260720
|
+
const tagsInline = YAML_TAGS_INLINE.exec(fmBlock);
|
|
260721
|
+
if (tagsInline) {
|
|
260722
|
+
tags.push(...tagsInline[1].split(",").map((t) => t.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean));
|
|
260723
|
+
} else if (YAML_TAGS_KEY.test(fmBlock)) {
|
|
260724
|
+
let itemMatch;
|
|
260725
|
+
const itemRegex = /^\s+-\s+(.+)$/gm;
|
|
260726
|
+
const tagsSection = fmBlock.slice(fmBlock.indexOf("tags:"));
|
|
260727
|
+
while ((itemMatch = itemRegex.exec(tagsSection)) !== null) {
|
|
260728
|
+
tags.push(itemMatch[1].trim().replace(/^['"]|['"]$/g, ""));
|
|
260729
|
+
}
|
|
260730
|
+
}
|
|
260731
|
+
const aliasMatch = YAML_ALIASES.exec(fmBlock);
|
|
260732
|
+
if (aliasMatch) {
|
|
260733
|
+
aliases.push(...aliasMatch[1].split(",").map((a) => a.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean));
|
|
260734
|
+
}
|
|
260735
|
+
return { tags, aliases };
|
|
260736
|
+
}
|
|
260737
|
+
// ─── Link Resolution ─────────────────────────────────────────────
|
|
260738
|
+
/**
|
|
260739
|
+
* Resolve an Obsidian wiki-link target to a file path.
|
|
260740
|
+
* Obsidian uses "shortest path when possible" — [[Page]] matches
|
|
260741
|
+
* any file named Page.md anywhere in the vault.
|
|
260742
|
+
*/
|
|
260743
|
+
resolveWikiLink(fromFile, target) {
|
|
260744
|
+
const dir = path2.dirname(fromFile);
|
|
260745
|
+
if (path2.extname(target)) {
|
|
260746
|
+
return this.tryResolve(dir, target);
|
|
260747
|
+
}
|
|
260748
|
+
const resolved = this.tryResolve(dir, target + ".md") ?? this.tryResolve(dir, target + ".mdx");
|
|
260749
|
+
if (resolved)
|
|
260750
|
+
return resolved;
|
|
260751
|
+
if (this.rootDir) {
|
|
260752
|
+
const found = this.findFileInVault(target);
|
|
260753
|
+
if (found)
|
|
260754
|
+
return found;
|
|
260755
|
+
}
|
|
260756
|
+
return null;
|
|
260757
|
+
}
|
|
260758
|
+
/** Resolve a standard relative markdown link */
|
|
260759
|
+
resolveRelativeLink(fromFile, linkPath) {
|
|
260760
|
+
const dir = path2.dirname(fromFile);
|
|
260761
|
+
const candidate = path2.resolve(dir, linkPath);
|
|
260762
|
+
if (fs.existsSync(candidate)) {
|
|
260763
|
+
return candidate.replace(/\\/g, "/");
|
|
260764
|
+
}
|
|
260765
|
+
return null;
|
|
260766
|
+
}
|
|
260767
|
+
/** Try resolving a path relative to the current directory or as absolute */
|
|
260768
|
+
tryResolve(dir, target) {
|
|
260769
|
+
const relative = path2.resolve(dir, target);
|
|
260770
|
+
if (fs.existsSync(relative)) {
|
|
260771
|
+
return relative.replace(/\\/g, "/");
|
|
260772
|
+
}
|
|
260773
|
+
if (this.rootDir) {
|
|
260774
|
+
const fromRoot = path2.resolve(this.rootDir, target);
|
|
260775
|
+
if (fs.existsSync(fromRoot)) {
|
|
260776
|
+
return fromRoot.replace(/\\/g, "/");
|
|
260777
|
+
}
|
|
260778
|
+
}
|
|
260779
|
+
return null;
|
|
260780
|
+
}
|
|
260781
|
+
/**
|
|
260782
|
+
* Search the vault (rootDir) recursively for a file matching the target name.
|
|
260783
|
+
* This implements Obsidian's "shortest path" resolution for [[Page]] links.
|
|
260784
|
+
*/
|
|
260785
|
+
findFileInVault(target) {
|
|
260786
|
+
if (!this.rootDir)
|
|
260787
|
+
return null;
|
|
260788
|
+
const targetLower = target.toLowerCase();
|
|
260789
|
+
const extensions = [".md", ".mdx"];
|
|
260790
|
+
const queue = [this.rootDir];
|
|
260791
|
+
const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", "build", ".obsidian"]);
|
|
260792
|
+
while (queue.length > 0) {
|
|
260793
|
+
const dir = queue.shift();
|
|
260794
|
+
let entries;
|
|
260795
|
+
try {
|
|
260796
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
260797
|
+
} catch {
|
|
260798
|
+
continue;
|
|
260799
|
+
}
|
|
260800
|
+
for (const entry of entries) {
|
|
260801
|
+
if (entry.isDirectory()) {
|
|
260802
|
+
if (!skip.has(entry.name)) {
|
|
260803
|
+
queue.push(path2.join(dir, entry.name));
|
|
260804
|
+
}
|
|
260805
|
+
} else if (entry.isFile()) {
|
|
260806
|
+
const name = path2.basename(entry.name, path2.extname(entry.name));
|
|
260807
|
+
const ext = path2.extname(entry.name).toLowerCase();
|
|
260808
|
+
if (name.toLowerCase() === targetLower && extensions.includes(ext)) {
|
|
260809
|
+
return path2.join(dir, entry.name).replace(/\\/g, "/");
|
|
260810
|
+
}
|
|
260811
|
+
}
|
|
260812
|
+
}
|
|
260813
|
+
}
|
|
260814
|
+
return null;
|
|
260815
|
+
}
|
|
260816
|
+
/** Strip frontmatter and fenced code blocks so link regexes don't match inside them */
|
|
260817
|
+
stripFrontmatterAndCode(source) {
|
|
260818
|
+
let body = source.replace(FRONTMATTER, "");
|
|
260819
|
+
body = body.replace(/```[\s\S]*?```/g, "");
|
|
260820
|
+
body = body.replace(/`[^`]*`/g, "");
|
|
260821
|
+
return body;
|
|
260822
|
+
}
|
|
260823
|
+
};
|
|
260824
|
+
exports2.MarkdownParser = MarkdownParser;
|
|
260825
|
+
}
|
|
260826
|
+
});
|
|
260827
|
+
|
|
260436
260828
|
// packages/parsers/dist/cross-network/http-call-detector.js
|
|
260437
260829
|
var require_http_call_detector = __commonJS({
|
|
260438
260830
|
"packages/parsers/dist/cross-network/http-call-detector.js"(exports2) {
|
|
@@ -260795,13 +261187,15 @@ var require_parser_registry = __commonJS({
|
|
|
260795
261187
|
var typescript_parser_1 = require_typescript_parser();
|
|
260796
261188
|
var python_parser_1 = require_python_parser();
|
|
260797
261189
|
var php_parser_1 = require_php_parser();
|
|
261190
|
+
var markdown_parser_1 = require_markdown_parser();
|
|
260798
261191
|
var cross_network_1 = require_cross_network();
|
|
260799
261192
|
var fs = __importStar(require("fs"));
|
|
260800
261193
|
var path2 = __importStar(require("path"));
|
|
260801
261194
|
var ignore_1 = __importDefault(require_ignore());
|
|
260802
261195
|
var pythonParser = new python_parser_1.PythonParser();
|
|
260803
261196
|
var phpParser = new php_parser_1.PhpParser();
|
|
260804
|
-
var
|
|
261197
|
+
var markdownParser = new markdown_parser_1.MarkdownParser();
|
|
261198
|
+
var parsers = [new typescript_parser_1.TypeScriptParser(), pythonParser, phpParser, markdownParser];
|
|
260805
261199
|
var ALWAYS_SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", "build"]);
|
|
260806
261200
|
function loadGitignore(rootDir) {
|
|
260807
261201
|
const ig = (0, ignore_1.default)();
|
|
@@ -260820,6 +261214,7 @@ var require_parser_registry = __commonJS({
|
|
|
260820
261214
|
const sourceByFileId = /* @__PURE__ */ new Map();
|
|
260821
261215
|
pythonParser.setRootDir(dirPath);
|
|
260822
261216
|
phpParser.setRootDir(dirPath);
|
|
261217
|
+
markdownParser.setRootDir(dirPath);
|
|
260823
261218
|
function walk(dir) {
|
|
260824
261219
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
260825
261220
|
for (const entry of entries) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zoldia/omnigraph",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "A multi-language, AST-driven dependency visualizer for complex codebases. Parses TypeScript/NestJS, Python/FastAPI/Django,
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "A multi-language, AST-driven dependency visualizer for complex codebases. Parses TypeScript/NestJS/Next.js, Python/FastAPI/Django, PHP/Laravel, and Markdown/Obsidian and renders an interactive dependency graph.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"omnigraph": "dist/cli.js"
|
|
7
7
|
},
|
|
@@ -19,12 +19,16 @@
|
|
|
19
19
|
"typescript",
|
|
20
20
|
"python",
|
|
21
21
|
"php",
|
|
22
|
+
"markdown",
|
|
22
23
|
"nestjs",
|
|
24
|
+
"nextjs",
|
|
23
25
|
"fastapi",
|
|
24
26
|
"django",
|
|
25
27
|
"laravel",
|
|
28
|
+
"obsidian",
|
|
29
|
+
"wiki-links",
|
|
26
30
|
"react-flow",
|
|
27
|
-
"
|
|
31
|
+
"gif-export"
|
|
28
32
|
],
|
|
29
33
|
"repository": {
|
|
30
34
|
"type": "git",
|