@zoldia/omnigraph 1.0.0 → 1.2.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/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 parsers = [new typescript_parser_1.TypeScriptParser(), pythonParser, phpParser];
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.0.0",
4
- "description": "A multi-language, AST-driven dependency visualizer for complex codebases. Parses TypeScript/NestJS, Python/FastAPI/Django, and PHP/Laravel and renders an interactive dependency graph.",
3
+ "version": "1.2.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,21 @@
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
- "obsidian"
31
+ "gif-export",
32
+ "erd",
33
+ "foreign-keys",
34
+ "cli",
35
+ "method-extraction",
36
+ "tsconfig-paths"
28
37
  ],
29
38
  "repository": {
30
39
  "type": "git",