gitnexus 1.6.4-rc.51 → 1.6.4-rc.53
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/core/group/config-parser.js +1 -1
- package/dist/core/group/extractors/elixir-workspace-extractor.d.ts +15 -0
- package/dist/core/group/extractors/elixir-workspace-extractor.js +203 -0
- package/dist/core/group/extractors/go-workspace-extractor.d.ts +14 -0
- package/dist/core/group/extractors/go-workspace-extractor.js +216 -0
- package/dist/core/group/extractors/java-workspace-extractor.d.ts +16 -0
- package/dist/core/group/extractors/java-workspace-extractor.js +203 -0
- package/dist/core/group/extractors/manifest-extractor.js +8 -6
- package/dist/core/group/extractors/node-workspace-extractor.d.ts +14 -0
- package/dist/core/group/extractors/node-workspace-extractor.js +206 -0
- package/dist/core/group/extractors/python-workspace-extractor.d.ts +15 -0
- package/dist/core/group/extractors/python-workspace-extractor.js +204 -0
- package/dist/core/group/extractors/workspace-extractor.d.ts +13 -0
- package/dist/core/group/extractors/workspace-extractor.js +65 -0
- package/dist/core/group/sync.js +5 -3
- package/package.json +1 -1
- package/web/assets/{agent-DItiqJRq.js → agent-DSzTymIk.js} +1 -1
- package/web/assets/architecture-YZFGNWBL-S5CXDPWN-D3kSwRPj.js +1 -0
- package/web/assets/{architectureDiagram-EMZXCZ2Q-CE8JvLQH.js → architectureDiagram-EMZXCZ2Q-UmClXAm6.js} +1 -1
- package/web/assets/{blockDiagram-IGV67L2C-hutf3-Rf.js → blockDiagram-IGV67L2C-Dq9GLiDP.js} +1 -1
- package/web/assets/{c4Diagram-DFAF54RM-CDgPwdI_.js → c4Diagram-DFAF54RM-BAulQDfB.js} +1 -1
- package/web/assets/{chunk-3GS5O3IE-Cx4rYy69.js → chunk-3GS5O3IE-DRE1sM7w.js} +1 -1
- package/web/assets/{chunk-3YCYZ6SJ-DWxw2F8B.js → chunk-3YCYZ6SJ-BkrQ98l_.js} +1 -1
- package/web/assets/{chunk-6NTNNK5N-CMelHKdt.js → chunk-6NTNNK5N-gx0rYVsL.js} +1 -1
- package/web/assets/{chunk-A34GCYZU-BdEY2RCQ.js → chunk-A34GCYZU-RZiTgPCZ.js} +1 -1
- package/web/assets/{chunk-DJ7UZH7F-PizSwWnc.js → chunk-DJ7UZH7F-BabYEcUv.js} +1 -1
- package/web/assets/{chunk-DKKBVRCY-BITgFJtT.js → chunk-DKKBVRCY-TALC3MOe.js} +2 -2
- package/web/assets/{chunk-DU5LTGQ6-DjSVNRly.js → chunk-DU5LTGQ6-DaoUQLBX.js} +1 -1
- package/web/assets/{chunk-FXACKDTF-BBdk0y0E.js → chunk-FXACKDTF-C7YCo_Ke.js} +1 -1
- package/web/assets/{chunk-H3VCZNTA-BuHjQqGv.js → chunk-H3VCZNTA-ClUTKG4X.js} +1 -1
- package/web/assets/{chunk-HN6EAY2L-BuEpUEf1.js → chunk-HN6EAY2L-CIB-EAEf.js} +1 -1
- package/web/assets/{chunk-O5ABG6QK-OD-Penaw.js → chunk-O5ABG6QK-pfHdaAl5.js} +1 -1
- package/web/assets/{chunk-PK6DOVAG-CuIS-eTi.js → chunk-PK6DOVAG-CP63Y-dO.js} +1 -1
- package/web/assets/{chunk-RNJOYNJ4-6sC89Gwg.js → chunk-RNJOYNJ4-u0uFowaT.js} +1 -1
- package/web/assets/{chunk-RWUO3TPN-Ckis4GEZ.js → chunk-RWUO3TPN-XaxD6IN5.js} +1 -1
- package/web/assets/{chunk-TBF5ZNIQ-BCXeRyum.js → chunk-TBF5ZNIQ-phr69mzb.js} +1 -1
- package/web/assets/{chunk-TYMNRAUI-ChcULBiI.js → chunk-TYMNRAUI-COznwoNo.js} +1 -1
- package/web/assets/{chunk-W7ZLLLMY-C9qAqU9J.js → chunk-W7ZLLLMY-ktiqXFzv.js} +1 -1
- package/web/assets/{chunk-WSB5WSVC-CBn8ynck.js → chunk-WSB5WSVC-BaodRn5r.js} +1 -1
- package/web/assets/{chunk-XGPFEOL4-CFcfign9.js → chunk-XGPFEOL4-DEWAJYtS.js} +1 -1
- package/web/assets/classDiagram-PPOCWD7C-DJm9V5Z9.js +1 -0
- package/web/assets/classDiagram-v2-23LJLIIU-CONC2q7O.js +1 -0
- package/web/assets/{cose-bilkent-PNC4W37J-DNEpbdTz.js → cose-bilkent-PNC4W37J-CdXmWYUn.js} +1 -1
- package/web/assets/{dagre-E77IOHMT-ClfJi3lO.js → dagre-E77IOHMT-85yFNa-U.js} +1 -1
- package/web/assets/{diagram-H7BISOXX-fo8UxfUM.js → diagram-H7BISOXX-DmNNIGoc.js} +1 -1
- package/web/assets/{diagram-JC5VWROH-Wjg9r4gO.js → diagram-JC5VWROH-BoASIDAQ.js} +1 -1
- package/web/assets/{diagram-LXUTUG65-BdSO4Vyi.js → diagram-LXUTUG65-D7_rWL46.js} +1 -1
- package/web/assets/{diagram-WEHSV5V5-ctouQZh3.js → diagram-WEHSV5V5-C65Uxmnr.js} +1 -1
- package/web/assets/{erDiagram-GCSMX5X6-DpcHlfNA.js → erDiagram-GCSMX5X6-C6iGqdrz.js} +1 -1
- package/web/assets/{flowDiagram-OTCZ4VVT-DdFWMLXt.js → flowDiagram-OTCZ4VVT-Dwqe7LwQ.js} +1 -1
- package/web/assets/{ganttDiagram-MUNLMDZQ-ChJRuWEN.js → ganttDiagram-MUNLMDZQ-BdPv-0c6.js} +1 -1
- package/web/assets/gitGraph-7Q5UKJZL-54BCDZD5-CHLZv53W.js +1 -0
- package/web/assets/{gitGraphDiagram-3HKGZ4G3-DCor76_B.js → gitGraphDiagram-3HKGZ4G3-CuH1Gh8s.js} +1 -1
- package/web/assets/{index-i8mZYWUD.js → index-DpMcvzVf.js} +41 -41
- package/web/assets/info-OMHHGYJF-BF2H5H6G-Yt27NUzo.js +1 -0
- package/web/assets/infoDiagram-MN7RKWGX-BDLC_czJ.js +2 -0
- package/web/assets/{ishikawaDiagram-YMYX4NHK-Bwr7h5yK.js → ishikawaDiagram-YMYX4NHK-5ASIVfH_.js} +1 -1
- package/web/assets/{journeyDiagram-SO5T7YLQ-B2DnC3r5.js → journeyDiagram-SO5T7YLQ-BqHCwWBv.js} +1 -1
- package/web/assets/{kanban-definition-LJHFXRCJ-CzQhupJ-.js → kanban-definition-LJHFXRCJ-Pf6kpbyo.js} +1 -1
- package/web/assets/{mindmap-definition-2EUWGEK5-CFfLfk9a.js → mindmap-definition-2EUWGEK5-4cvOOu2O.js} +1 -1
- package/web/assets/packet-4T2RLAQJ-EV4IVRXR-BGlzFMOr.js +1 -0
- package/web/assets/pie-ZZUOXDRM-N23DN5KN-Ui7Mc58L.js +1 -0
- package/web/assets/{pieDiagram-3IATQBI2-BNtFly7P.js → pieDiagram-3IATQBI2-CZ7v2kzW.js} +1 -1
- package/web/assets/{quadrantDiagram-E256RVCF-BbI_ZPSF.js → quadrantDiagram-E256RVCF-XDUp3YFm.js} +1 -1
- package/web/assets/radar-PYXPWWZC-P6TP7ZYP-0jZkUeeE.js +1 -0
- package/web/assets/{requirementDiagram-M5DCFWZL-B7oRUeTm.js → requirementDiagram-M5DCFWZL-DAKpbdmn.js} +1 -1
- package/web/assets/{sankeyDiagram-L3NBLAOT-BeOC_s-g.js → sankeyDiagram-L3NBLAOT-Cuh_Q_Wz.js} +1 -1
- package/web/assets/{sequenceDiagram-ZOUHS735-DtXc1cq6.js → sequenceDiagram-ZOUHS735-nFqoCrOa.js} +1 -1
- package/web/assets/{stateDiagram-MLPALWAM-uawYkYoH.js → stateDiagram-MLPALWAM-Drv05fh-.js} +1 -1
- package/web/assets/stateDiagram-v2-B5LQ5ZB2-DeIxIjc2.js +1 -0
- package/web/assets/{timeline-definition-5SPVSISX-CiFVDrrG.js → timeline-definition-5SPVSISX-BLh-flCC.js} +1 -1
- package/web/assets/treeView-SZITEDCU-5DXDK3XO-TMf1DsPD.js +1 -0
- package/web/assets/treemap-W4RFUUIX-WYLRDWKO-DTLiM44i.js +1 -0
- package/web/assets/{vennDiagram-IE5QUKF5-DCyQqwx7.js → vennDiagram-IE5QUKF5-C5S0N3Qi.js} +1 -1
- package/web/assets/wardley-RL74JXVD-BCRCBASE-BLQ7nRDD.js +1 -0
- package/web/assets/{wardleyDiagram-XU3VSMPF-Bus6fmr-.js → wardleyDiagram-XU3VSMPF-BMGe2aBh.js} +1 -1
- package/web/assets/{xychartDiagram-ZHJ5623Y-DU_290J8.js → xychartDiagram-ZHJ5623Y-9g_anVQx.js} +1 -1
- package/web/index.html +1 -1
- package/web/assets/architecture-YZFGNWBL-S5CXDPWN-BVNYZR02.js +0 -1
- package/web/assets/classDiagram-PPOCWD7C-DP-FXqh2.js +0 -1
- package/web/assets/classDiagram-v2-23LJLIIU-C7DWKMLR.js +0 -1
- package/web/assets/gitGraph-7Q5UKJZL-54BCDZD5-CicCrLIW.js +0 -1
- package/web/assets/info-OMHHGYJF-BF2H5H6G-C7DIIhaB.js +0 -1
- package/web/assets/infoDiagram-MN7RKWGX-Bhgkk7XM.js +0 -2
- package/web/assets/packet-4T2RLAQJ-EV4IVRXR-DtczixRj.js +0 -1
- package/web/assets/pie-ZZUOXDRM-N23DN5KN-DroTvcJ-.js +0 -1
- package/web/assets/radar-PYXPWWZC-P6TP7ZYP-Bl2tr5_W.js +0 -1
- package/web/assets/stateDiagram-v2-B5LQ5ZB2-YUx4_NOp.js +0 -1
- package/web/assets/treeView-SZITEDCU-5DXDK3XO-p1_8zFCT.js +0 -1
- package/web/assets/treemap-W4RFUUIX-WYLRDWKO-DSuXY6Cw.js +0 -1
- package/web/assets/wardley-RL74JXVD-BCRCBASE-D89VeyOM.js +0 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CypherExecutor } from '../contract-extractor.js';
|
|
2
|
+
import type { GroupManifestLink } from '../types.js';
|
|
3
|
+
interface ElixirAppMeta {
|
|
4
|
+
appName: string;
|
|
5
|
+
modulePrefix: string;
|
|
6
|
+
groupPath: string;
|
|
7
|
+
repoPath: string;
|
|
8
|
+
deps: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface ElixirWorkspaceResult {
|
|
11
|
+
links: GroupManifestLink[];
|
|
12
|
+
discoveredApps: Map<string, ElixirAppMeta>;
|
|
13
|
+
}
|
|
14
|
+
export declare function extractElixirWorkspaceLinks(repos: Record<string, string>, repoPaths: Map<string, string>, _dbExecutors?: Map<string, CypherExecutor>): Promise<ElixirWorkspaceResult>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { shouldIgnorePath, loadIgnoreRules } from '../../../config/ignore-service.js';
|
|
4
|
+
async function parseMixExs(repoPath) {
|
|
5
|
+
const mixPath = path.join(repoPath, 'mix.exs');
|
|
6
|
+
let content;
|
|
7
|
+
try {
|
|
8
|
+
content = await fs.readFile(mixPath, 'utf-8');
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
// app: :my_app
|
|
14
|
+
const appMatch = content.match(/app:\s*:(\w+)/);
|
|
15
|
+
if (!appMatch)
|
|
16
|
+
return null;
|
|
17
|
+
const appName = appMatch[1];
|
|
18
|
+
// Derive module prefix: my_app -> MyApp
|
|
19
|
+
const modulePrefix = appName
|
|
20
|
+
.split('_')
|
|
21
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
22
|
+
.join('');
|
|
23
|
+
const deps = [];
|
|
24
|
+
// {:dep_name, "~> 1.0"} or {:dep_name, in_umbrella: true}
|
|
25
|
+
// {:dep_name, git: "..."} or {:dep_name, path: "..."}
|
|
26
|
+
const depMatches = content.matchAll(/\{:(\w+)\s*,\s*(?:"[^"]*"|~[^}]*|[^}]*(?:in_umbrella|path|git)\s*:[^}]*)\}/g);
|
|
27
|
+
for (const m of depMatches) {
|
|
28
|
+
deps.push(m[1]);
|
|
29
|
+
}
|
|
30
|
+
return { appName, modulePrefix, deps: [...new Set(deps)] };
|
|
31
|
+
}
|
|
32
|
+
async function scanElixirImports(repoPath, knownApps) {
|
|
33
|
+
const results = [];
|
|
34
|
+
const sourceFiles = await findElixirFiles(repoPath);
|
|
35
|
+
for (const relFile of sourceFiles) {
|
|
36
|
+
const absPath = path.join(repoPath, relFile);
|
|
37
|
+
let content;
|
|
38
|
+
try {
|
|
39
|
+
content = await fs.readFile(absPath, 'utf-8');
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
// alias MyApp.SomeModule
|
|
45
|
+
// alias MyApp.SomeModule, as: Short
|
|
46
|
+
// alias MyApp.{ModA, ModB}
|
|
47
|
+
const aliasRegex = /^\s*alias\s+([A-Z]\w+(?:\.[A-Z]\w+)*(?:\.\{[^}]+\})?)/gm;
|
|
48
|
+
let match;
|
|
49
|
+
while ((match = aliasRegex.exec(content)) !== null) {
|
|
50
|
+
const aliasExpr = match[1];
|
|
51
|
+
const modules = expandAlias(aliasExpr);
|
|
52
|
+
for (const mod of modules) {
|
|
53
|
+
const appName = matchModuleToApp(mod, knownApps);
|
|
54
|
+
if (appName) {
|
|
55
|
+
results.push({ appName, moduleName: mod, filePath: relFile });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Direct module reference: MyApp.Module.func() or MyApp.Module
|
|
60
|
+
// Strip comment lines and string literals to avoid false positives
|
|
61
|
+
const codeOnly = content
|
|
62
|
+
.split('\n')
|
|
63
|
+
.filter((line) => !line.trimStart().startsWith('#'))
|
|
64
|
+
.join('\n');
|
|
65
|
+
for (const [prefix, appName] of knownApps) {
|
|
66
|
+
const refRegex = new RegExp(`\\b(${escapeRegex(prefix)}\\.[A-Z][A-Za-z0-9]*(?:\\.[A-Z][A-Za-z0-9]*)*)`, 'g');
|
|
67
|
+
while ((match = refRegex.exec(codeOnly)) !== null) {
|
|
68
|
+
const mod = match[1];
|
|
69
|
+
if (!results.some((r) => r.moduleName === mod && r.filePath === relFile)) {
|
|
70
|
+
results.push({ appName, moduleName: mod, filePath: relFile });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return results;
|
|
76
|
+
}
|
|
77
|
+
function expandAlias(expr) {
|
|
78
|
+
const braceMatch = expr.match(/^([A-Z][\w.]*)\.\{([^}]+)\}$/);
|
|
79
|
+
if (braceMatch) {
|
|
80
|
+
const prefix = braceMatch[1];
|
|
81
|
+
return braceMatch[2]
|
|
82
|
+
.split(',')
|
|
83
|
+
.map((s) => s.trim())
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
.map((s) => `${prefix}.${s}`);
|
|
86
|
+
}
|
|
87
|
+
return [expr];
|
|
88
|
+
}
|
|
89
|
+
function matchModuleToApp(moduleName, knownApps) {
|
|
90
|
+
for (const [prefix, appName] of knownApps) {
|
|
91
|
+
if (moduleName === prefix || moduleName.startsWith(prefix + '.')) {
|
|
92
|
+
return appName;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
function escapeRegex(s) {
|
|
98
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
99
|
+
}
|
|
100
|
+
function extractTopModule(moduleName, prefix) {
|
|
101
|
+
const rest = moduleName.slice(prefix.length);
|
|
102
|
+
if (!rest || rest === '.')
|
|
103
|
+
return moduleName;
|
|
104
|
+
const afterDot = rest.startsWith('.') ? rest.slice(1) : rest;
|
|
105
|
+
const parts = afterDot.split('.');
|
|
106
|
+
return `${prefix}.${parts[0]}`;
|
|
107
|
+
}
|
|
108
|
+
async function findElixirFiles(repoPath) {
|
|
109
|
+
const results = [];
|
|
110
|
+
const ig = await loadIgnoreRules(repoPath);
|
|
111
|
+
async function walk(dir, rel) {
|
|
112
|
+
let entries;
|
|
113
|
+
try {
|
|
114
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
121
|
+
if (entry.isDirectory()) {
|
|
122
|
+
if (shouldIgnorePath(childRel))
|
|
123
|
+
continue;
|
|
124
|
+
if (ig && ig.ignores(childRel + '/'))
|
|
125
|
+
continue;
|
|
126
|
+
await walk(path.join(dir, entry.name), childRel);
|
|
127
|
+
}
|
|
128
|
+
else if (entry.name.endsWith('.ex') || entry.name.endsWith('.exs')) {
|
|
129
|
+
if (entry.name === 'mix.exs' || entry.name === 'mix.lock')
|
|
130
|
+
continue;
|
|
131
|
+
if (shouldIgnorePath(childRel))
|
|
132
|
+
continue;
|
|
133
|
+
if (ig && ig.ignores(childRel))
|
|
134
|
+
continue;
|
|
135
|
+
results.push(childRel);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
await walk(repoPath, '');
|
|
140
|
+
return results;
|
|
141
|
+
}
|
|
142
|
+
export async function extractElixirWorkspaceLinks(repos, repoPaths, _dbExecutors) {
|
|
143
|
+
const appsByName = new Map();
|
|
144
|
+
const appsByGroupPath = new Map();
|
|
145
|
+
for (const [groupPath] of Object.entries(repos)) {
|
|
146
|
+
const repoPath = repoPaths.get(groupPath);
|
|
147
|
+
if (!repoPath)
|
|
148
|
+
continue;
|
|
149
|
+
const manifest = await parseMixExs(repoPath);
|
|
150
|
+
if (!manifest)
|
|
151
|
+
continue;
|
|
152
|
+
const meta = {
|
|
153
|
+
appName: manifest.appName,
|
|
154
|
+
modulePrefix: manifest.modulePrefix,
|
|
155
|
+
groupPath,
|
|
156
|
+
repoPath,
|
|
157
|
+
deps: manifest.deps,
|
|
158
|
+
};
|
|
159
|
+
const existing = appsByName.get(manifest.appName);
|
|
160
|
+
if (existing) {
|
|
161
|
+
console.warn(`[elixir-workspace-extractor] duplicate app "${manifest.appName}" in "${groupPath}" and "${existing.groupPath}" — skipping "${groupPath}"`);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
appsByName.set(manifest.appName, meta);
|
|
165
|
+
appsByGroupPath.set(groupPath, meta);
|
|
166
|
+
}
|
|
167
|
+
const links = [];
|
|
168
|
+
const seen = new Set();
|
|
169
|
+
for (const [, app] of appsByGroupPath) {
|
|
170
|
+
const groupDeps = app.deps.filter((d) => appsByName.has(d));
|
|
171
|
+
if (groupDeps.length === 0)
|
|
172
|
+
continue;
|
|
173
|
+
const knownApps = new Map();
|
|
174
|
+
for (const dep of groupDeps) {
|
|
175
|
+
const depMeta = appsByName.get(dep);
|
|
176
|
+
if (depMeta)
|
|
177
|
+
knownApps.set(depMeta.modulePrefix, dep);
|
|
178
|
+
}
|
|
179
|
+
const imports = await scanElixirImports(app.repoPath, knownApps);
|
|
180
|
+
for (const imp of imports) {
|
|
181
|
+
const providerApp = appsByName.get(imp.appName);
|
|
182
|
+
if (!providerApp)
|
|
183
|
+
continue;
|
|
184
|
+
const topModule = extractTopModule(imp.moduleName, providerApp.modulePrefix);
|
|
185
|
+
const key = `${app.groupPath}→${providerApp.groupPath}::${topModule}`;
|
|
186
|
+
if (seen.has(key))
|
|
187
|
+
continue;
|
|
188
|
+
seen.add(key);
|
|
189
|
+
// V1: Elixir contracts use the full module name (e.g. "Core.Schema") without
|
|
190
|
+
// an "appName::" prefix. resolveSymbol will query the graph with this full
|
|
191
|
+
// string — resolution depends on Elixir indexer storing fully-qualified names.
|
|
192
|
+
const link = {
|
|
193
|
+
from: providerApp.groupPath,
|
|
194
|
+
to: app.groupPath,
|
|
195
|
+
type: 'custom',
|
|
196
|
+
contract: topModule,
|
|
197
|
+
role: 'provider',
|
|
198
|
+
};
|
|
199
|
+
links.push(link);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return { links, discoveredApps: appsByGroupPath };
|
|
203
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CypherExecutor } from '../contract-extractor.js';
|
|
2
|
+
import type { GroupManifestLink } from '../types.js';
|
|
3
|
+
interface GoModuleMeta {
|
|
4
|
+
modulePath: string;
|
|
5
|
+
groupPath: string;
|
|
6
|
+
repoPath: string;
|
|
7
|
+
requires: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface GoWorkspaceResult {
|
|
10
|
+
links: GroupManifestLink[];
|
|
11
|
+
discoveredModules: Map<string, GoModuleMeta>;
|
|
12
|
+
}
|
|
13
|
+
export declare function extractGoWorkspaceLinks(repos: Record<string, string>, repoPaths: Map<string, string>, _dbExecutors?: Map<string, CypherExecutor>): Promise<GoWorkspaceResult>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { shouldIgnorePath, loadIgnoreRules } from '../../../config/ignore-service.js';
|
|
4
|
+
async function parseGoMod(repoPath) {
|
|
5
|
+
const goModPath = path.join(repoPath, 'go.mod');
|
|
6
|
+
let content;
|
|
7
|
+
try {
|
|
8
|
+
content = await fs.readFile(goModPath, 'utf-8');
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const moduleMatch = content.match(/^module\s+(\S+)/m);
|
|
14
|
+
if (!moduleMatch)
|
|
15
|
+
return null;
|
|
16
|
+
const modulePath = moduleMatch[1];
|
|
17
|
+
const requires = [];
|
|
18
|
+
// Single-line: require github.com/org/repo v1.2.3
|
|
19
|
+
const singleReqs = content.matchAll(/^require\s+(\S+)\s+/gm);
|
|
20
|
+
for (const m of singleReqs)
|
|
21
|
+
requires.push(m[1]);
|
|
22
|
+
// Block: require ( ... )
|
|
23
|
+
const blockReqs = content.matchAll(/^require\s*\(\s*\n([\s\S]*?)\)/gm);
|
|
24
|
+
for (const block of blockReqs) {
|
|
25
|
+
const lines = block[1].split('\n');
|
|
26
|
+
for (const line of lines) {
|
|
27
|
+
const trimmed = line.trim();
|
|
28
|
+
if (!trimmed || trimmed.startsWith('//'))
|
|
29
|
+
continue;
|
|
30
|
+
const parts = trimmed.split(/\s+/);
|
|
31
|
+
if (parts[0])
|
|
32
|
+
requires.push(parts[0]);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// replace directives (local path deps)
|
|
36
|
+
const replaceLines = content.matchAll(/^replace\s+(\S+)\s+=>\s+\.\//gm);
|
|
37
|
+
for (const m of replaceLines) {
|
|
38
|
+
if (!requires.includes(m[1]))
|
|
39
|
+
requires.push(m[1]);
|
|
40
|
+
}
|
|
41
|
+
const replaceBlocks = content.matchAll(/^replace\s*\(\s*\n([\s\S]*?)\)/gm);
|
|
42
|
+
for (const block of replaceBlocks) {
|
|
43
|
+
const lines = block[1].split('\n');
|
|
44
|
+
for (const line of lines) {
|
|
45
|
+
const trimmed = line.trim();
|
|
46
|
+
if (!trimmed || trimmed.startsWith('//'))
|
|
47
|
+
continue;
|
|
48
|
+
const match = trimmed.match(/^(\S+)\s+=>\s+\.\//);
|
|
49
|
+
if (match && !requires.includes(match[1]))
|
|
50
|
+
requires.push(match[1]);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { modulePath, requires: [...new Set(requires)] };
|
|
54
|
+
}
|
|
55
|
+
async function scanGoImports(repoPath, knownModules) {
|
|
56
|
+
const results = [];
|
|
57
|
+
const sourceFiles = await findGoFiles(repoPath);
|
|
58
|
+
for (const relFile of sourceFiles) {
|
|
59
|
+
const absPath = path.join(repoPath, relFile);
|
|
60
|
+
let content;
|
|
61
|
+
try {
|
|
62
|
+
content = await fs.readFile(absPath, 'utf-8');
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const importPaths = extractImportPaths(content);
|
|
68
|
+
for (const importPath of importPaths) {
|
|
69
|
+
const matchedModule = findMatchingModule(importPath, knownModules);
|
|
70
|
+
if (!matchedModule)
|
|
71
|
+
continue;
|
|
72
|
+
const symbols = extractUsedTypes(content, importPath);
|
|
73
|
+
for (const sym of symbols) {
|
|
74
|
+
results.push({ modulePath: matchedModule, symbolName: sym, filePath: relFile });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return results;
|
|
79
|
+
}
|
|
80
|
+
function extractImportPaths(content) {
|
|
81
|
+
const paths = [];
|
|
82
|
+
// Single: import "path"
|
|
83
|
+
const singleImports = content.matchAll(/^import\s+"([^"]+)"/gm);
|
|
84
|
+
for (const m of singleImports)
|
|
85
|
+
paths.push(m[1]);
|
|
86
|
+
// Single aliased: import alias "path"
|
|
87
|
+
const aliasedImports = content.matchAll(/^import\s+\w+\s+"([^"]+)"/gm);
|
|
88
|
+
for (const m of aliasedImports)
|
|
89
|
+
paths.push(m[1]);
|
|
90
|
+
// Block: import ( ... )
|
|
91
|
+
const blockImports = content.matchAll(/^import\s*\(\s*\n([\s\S]*?)\)/gm);
|
|
92
|
+
for (const block of blockImports) {
|
|
93
|
+
const lines = block[1].split('\n');
|
|
94
|
+
for (const line of lines) {
|
|
95
|
+
const trimmed = line.trim();
|
|
96
|
+
if (!trimmed || trimmed.startsWith('//'))
|
|
97
|
+
continue;
|
|
98
|
+
const pathMatch = trimmed.match(/"([^"]+)"/);
|
|
99
|
+
if (pathMatch)
|
|
100
|
+
paths.push(pathMatch[1]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return [...new Set(paths)];
|
|
104
|
+
}
|
|
105
|
+
function findMatchingModule(importPath, knownModules) {
|
|
106
|
+
for (const [modPath] of knownModules) {
|
|
107
|
+
if (importPath === modPath || importPath.startsWith(modPath + '/')) {
|
|
108
|
+
return modPath;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
function extractUsedTypes(content, importPath) {
|
|
114
|
+
const pkgName = importPath.split('/').pop() || '';
|
|
115
|
+
if (!pkgName)
|
|
116
|
+
return [];
|
|
117
|
+
// Match pkg.TypeName where TypeName is PascalCase (exported)
|
|
118
|
+
const typeRegex = new RegExp(`\\b${escapeRegex(pkgName)}\\.([A-Z][A-Za-z0-9]*)`, 'g');
|
|
119
|
+
const types = new Set();
|
|
120
|
+
let match;
|
|
121
|
+
while ((match = typeRegex.exec(content)) !== null) {
|
|
122
|
+
types.add(match[1]);
|
|
123
|
+
}
|
|
124
|
+
return [...types];
|
|
125
|
+
}
|
|
126
|
+
function escapeRegex(s) {
|
|
127
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
128
|
+
}
|
|
129
|
+
async function findGoFiles(repoPath) {
|
|
130
|
+
const results = [];
|
|
131
|
+
const ig = await loadIgnoreRules(repoPath);
|
|
132
|
+
async function walk(dir, rel) {
|
|
133
|
+
let entries;
|
|
134
|
+
try {
|
|
135
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
142
|
+
if (entry.isDirectory()) {
|
|
143
|
+
if (shouldIgnorePath(childRel))
|
|
144
|
+
continue;
|
|
145
|
+
if (ig && ig.ignores(childRel + '/'))
|
|
146
|
+
continue;
|
|
147
|
+
await walk(path.join(dir, entry.name), childRel);
|
|
148
|
+
}
|
|
149
|
+
else if (entry.name.endsWith('.go') && !entry.name.endsWith('_test.go')) {
|
|
150
|
+
if (shouldIgnorePath(childRel))
|
|
151
|
+
continue;
|
|
152
|
+
if (ig && ig.ignores(childRel))
|
|
153
|
+
continue;
|
|
154
|
+
results.push(childRel);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
await walk(repoPath, '');
|
|
159
|
+
return results;
|
|
160
|
+
}
|
|
161
|
+
export async function extractGoWorkspaceLinks(repos, repoPaths, _dbExecutors) {
|
|
162
|
+
const modulesByPath = new Map();
|
|
163
|
+
const modulesByGroupPath = new Map();
|
|
164
|
+
for (const [groupPath] of Object.entries(repos)) {
|
|
165
|
+
const repoPath = repoPaths.get(groupPath);
|
|
166
|
+
if (!repoPath)
|
|
167
|
+
continue;
|
|
168
|
+
const manifest = await parseGoMod(repoPath);
|
|
169
|
+
if (!manifest)
|
|
170
|
+
continue;
|
|
171
|
+
const meta = {
|
|
172
|
+
modulePath: manifest.modulePath,
|
|
173
|
+
groupPath,
|
|
174
|
+
repoPath,
|
|
175
|
+
requires: manifest.requires,
|
|
176
|
+
};
|
|
177
|
+
const existing = modulesByPath.get(manifest.modulePath);
|
|
178
|
+
if (existing) {
|
|
179
|
+
console.warn(`[go-workspace-extractor] duplicate module "${manifest.modulePath}" in "${groupPath}" and "${existing.groupPath}" — skipping "${groupPath}"`);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
modulesByPath.set(manifest.modulePath, meta);
|
|
183
|
+
modulesByGroupPath.set(groupPath, meta);
|
|
184
|
+
}
|
|
185
|
+
const links = [];
|
|
186
|
+
const seen = new Set();
|
|
187
|
+
for (const [, mod] of modulesByGroupPath) {
|
|
188
|
+
const groupModDeps = mod.requires.filter((r) => modulesByPath.has(r));
|
|
189
|
+
if (groupModDeps.length === 0)
|
|
190
|
+
continue;
|
|
191
|
+
const knownModules = new Map();
|
|
192
|
+
for (const dep of groupModDeps) {
|
|
193
|
+
knownModules.set(dep, dep);
|
|
194
|
+
}
|
|
195
|
+
const imports = await scanGoImports(mod.repoPath, knownModules);
|
|
196
|
+
for (const imp of imports) {
|
|
197
|
+
const providerMod = modulesByPath.get(imp.modulePath);
|
|
198
|
+
if (!providerMod)
|
|
199
|
+
continue;
|
|
200
|
+
const qualifiedContract = `${imp.modulePath}::${imp.symbolName}`;
|
|
201
|
+
const key = `${mod.groupPath}→${providerMod.groupPath}::${qualifiedContract}`;
|
|
202
|
+
if (seen.has(key))
|
|
203
|
+
continue;
|
|
204
|
+
seen.add(key);
|
|
205
|
+
const link = {
|
|
206
|
+
from: providerMod.groupPath,
|
|
207
|
+
to: mod.groupPath,
|
|
208
|
+
type: 'custom',
|
|
209
|
+
contract: qualifiedContract,
|
|
210
|
+
role: 'provider',
|
|
211
|
+
};
|
|
212
|
+
links.push(link);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return { links, discoveredModules: modulesByGroupPath };
|
|
216
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CypherExecutor } from '../contract-extractor.js';
|
|
2
|
+
import type { GroupManifestLink } from '../types.js';
|
|
3
|
+
interface JavaProjectMeta {
|
|
4
|
+
groupId: string;
|
|
5
|
+
artifactId: string;
|
|
6
|
+
basePackage: string;
|
|
7
|
+
groupPath: string;
|
|
8
|
+
repoPath: string;
|
|
9
|
+
deps: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface JavaWorkspaceResult {
|
|
12
|
+
links: GroupManifestLink[];
|
|
13
|
+
discoveredProjects: Map<string, JavaProjectMeta>;
|
|
14
|
+
}
|
|
15
|
+
export declare function extractJavaWorkspaceLinks(repos: Record<string, string>, repoPaths: Map<string, string>, _dbExecutors?: Map<string, CypherExecutor>): Promise<JavaWorkspaceResult>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { shouldIgnorePath, loadIgnoreRules } from '../../../config/ignore-service.js';
|
|
4
|
+
async function parseJavaManifest(repoPath) {
|
|
5
|
+
const pomPath = path.join(repoPath, 'pom.xml');
|
|
6
|
+
try {
|
|
7
|
+
const content = await fs.readFile(pomPath, 'utf-8');
|
|
8
|
+
return parsePom(content);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
// fall through to Gradle
|
|
12
|
+
}
|
|
13
|
+
for (const name of ['build.gradle.kts', 'build.gradle']) {
|
|
14
|
+
const gradlePath = path.join(repoPath, name);
|
|
15
|
+
try {
|
|
16
|
+
const content = await fs.readFile(gradlePath, 'utf-8');
|
|
17
|
+
return parseGradle(content, repoPath);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
function parsePom(content) {
|
|
26
|
+
const projectGroupMatch = content.match(/<project[^>]*>[\s\S]*?<groupId>([^<]+)<\/groupId>/);
|
|
27
|
+
const projectArtifactMatch = content.match(/<project[^>]*>[\s\S]*?<artifactId>([^<]+)<\/artifactId>/);
|
|
28
|
+
if (!projectGroupMatch || !projectArtifactMatch)
|
|
29
|
+
return null;
|
|
30
|
+
const groupId = projectGroupMatch[1].trim();
|
|
31
|
+
const artifactId = projectArtifactMatch[1].trim();
|
|
32
|
+
const deps = [];
|
|
33
|
+
const depBlocks = content.matchAll(/<dependency>\s*([\s\S]*?)<\/dependency>/g);
|
|
34
|
+
for (const block of depBlocks) {
|
|
35
|
+
const gMatch = block[1].match(/<groupId>([^<]+)<\/groupId>/);
|
|
36
|
+
const aMatch = block[1].match(/<artifactId>([^<]+)<\/artifactId>/);
|
|
37
|
+
if (gMatch && aMatch) {
|
|
38
|
+
deps.push(`${gMatch[1].trim()}:${aMatch[1].trim()}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { groupId, artifactId, deps: [...new Set(deps)] };
|
|
42
|
+
}
|
|
43
|
+
function parseGradle(content, repoPath) {
|
|
44
|
+
const groupMatch = content.match(/group\s*=\s*['"]([^'"]+)['"]/);
|
|
45
|
+
const dirName = path.basename(repoPath);
|
|
46
|
+
const groupId = groupMatch ? groupMatch[1] : '';
|
|
47
|
+
if (!groupId)
|
|
48
|
+
return null;
|
|
49
|
+
const artifactId = dirName;
|
|
50
|
+
const deps = [];
|
|
51
|
+
// implementation("group:artifact:version") or api("group:artifact:version")
|
|
52
|
+
const depMatches = content.matchAll(/(?:implementation|api|compileOnly|runtimeOnly)\s*\(\s*['"]([^'"]+)['"]\s*\)/g);
|
|
53
|
+
for (const m of depMatches) {
|
|
54
|
+
const parts = m[1].split(':');
|
|
55
|
+
if (parts.length >= 2) {
|
|
56
|
+
deps.push(`${parts[0]}:${parts[1]}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// implementation(project(":subproject"))
|
|
60
|
+
const projDeps = content.matchAll(/(?:implementation|api)\s*\(\s*project\s*\(\s*['"]([^'"]+)['"]\s*\)\s*\)/g);
|
|
61
|
+
for (const m of projDeps) {
|
|
62
|
+
const subName = m[1].replace(/^:/, '');
|
|
63
|
+
deps.push(`${groupId}:${subName}`);
|
|
64
|
+
}
|
|
65
|
+
return { groupId, artifactId, deps: [...new Set(deps)] };
|
|
66
|
+
}
|
|
67
|
+
function deriveBasePackage(groupId, artifactId) {
|
|
68
|
+
const sanitized = artifactId.replace(/-/g, '.');
|
|
69
|
+
if (groupId.endsWith(`.${sanitized}`) || groupId === sanitized) {
|
|
70
|
+
return groupId;
|
|
71
|
+
}
|
|
72
|
+
return `${groupId}.${sanitized}`;
|
|
73
|
+
}
|
|
74
|
+
async function scanJavaImports(repoPath, knownPackages) {
|
|
75
|
+
const results = [];
|
|
76
|
+
const sourceFiles = await findJavaFiles(repoPath);
|
|
77
|
+
for (const relFile of sourceFiles) {
|
|
78
|
+
const absPath = path.join(repoPath, relFile);
|
|
79
|
+
let content;
|
|
80
|
+
try {
|
|
81
|
+
content = await fs.readFile(absPath, 'utf-8');
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const importRegex = /^import\s+(?:static\s+)?([a-zA-Z][\w.]*\.[A-Z]\w*)/gm;
|
|
87
|
+
let match;
|
|
88
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
89
|
+
const fullImport = match[1];
|
|
90
|
+
for (const [basePkg, artifactKey] of knownPackages) {
|
|
91
|
+
if (fullImport.startsWith(basePkg + '.') || fullImport === basePkg) {
|
|
92
|
+
const parts = fullImport.split('.');
|
|
93
|
+
const className = parts[parts.length - 1];
|
|
94
|
+
if (isPascalCase(className)) {
|
|
95
|
+
results.push({
|
|
96
|
+
artifactKey,
|
|
97
|
+
symbolName: className,
|
|
98
|
+
filePath: relFile,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return results;
|
|
107
|
+
}
|
|
108
|
+
function isPascalCase(name) {
|
|
109
|
+
return /^[A-Z][A-Za-z0-9]*$/.test(name);
|
|
110
|
+
}
|
|
111
|
+
async function findJavaFiles(repoPath) {
|
|
112
|
+
const results = [];
|
|
113
|
+
const ig = await loadIgnoreRules(repoPath);
|
|
114
|
+
async function walk(dir, rel) {
|
|
115
|
+
let entries;
|
|
116
|
+
try {
|
|
117
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
124
|
+
if (entry.isDirectory()) {
|
|
125
|
+
if (shouldIgnorePath(childRel))
|
|
126
|
+
continue;
|
|
127
|
+
if (ig && ig.ignores(childRel + '/'))
|
|
128
|
+
continue;
|
|
129
|
+
await walk(path.join(dir, entry.name), childRel);
|
|
130
|
+
}
|
|
131
|
+
else if (entry.name.endsWith('.java') || entry.name.endsWith('.kt')) {
|
|
132
|
+
if (shouldIgnorePath(childRel))
|
|
133
|
+
continue;
|
|
134
|
+
if (ig && ig.ignores(childRel))
|
|
135
|
+
continue;
|
|
136
|
+
results.push(childRel);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
await walk(repoPath, '');
|
|
141
|
+
return results;
|
|
142
|
+
}
|
|
143
|
+
export async function extractJavaWorkspaceLinks(repos, repoPaths, _dbExecutors) {
|
|
144
|
+
const projectsByKey = new Map();
|
|
145
|
+
const projectsByGroupPath = new Map();
|
|
146
|
+
for (const [groupPath] of Object.entries(repos)) {
|
|
147
|
+
const repoPath = repoPaths.get(groupPath);
|
|
148
|
+
if (!repoPath)
|
|
149
|
+
continue;
|
|
150
|
+
const manifest = await parseJavaManifest(repoPath);
|
|
151
|
+
if (!manifest)
|
|
152
|
+
continue;
|
|
153
|
+
const key = `${manifest.groupId}:${manifest.artifactId}`;
|
|
154
|
+
const meta = {
|
|
155
|
+
groupId: manifest.groupId,
|
|
156
|
+
artifactId: manifest.artifactId,
|
|
157
|
+
basePackage: deriveBasePackage(manifest.groupId, manifest.artifactId),
|
|
158
|
+
groupPath,
|
|
159
|
+
repoPath,
|
|
160
|
+
deps: manifest.deps,
|
|
161
|
+
};
|
|
162
|
+
const existing = projectsByKey.get(key);
|
|
163
|
+
if (existing) {
|
|
164
|
+
console.warn(`[java-workspace-extractor] duplicate artifact "${key}" in "${groupPath}" and "${existing.groupPath}" — skipping "${groupPath}"`);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
projectsByKey.set(key, meta);
|
|
168
|
+
projectsByGroupPath.set(groupPath, meta);
|
|
169
|
+
}
|
|
170
|
+
const links = [];
|
|
171
|
+
const seen = new Set();
|
|
172
|
+
for (const [, proj] of projectsByGroupPath) {
|
|
173
|
+
const groupDeps = proj.deps.filter((d) => projectsByKey.has(d));
|
|
174
|
+
if (groupDeps.length === 0)
|
|
175
|
+
continue;
|
|
176
|
+
const knownPackages = new Map();
|
|
177
|
+
for (const dep of groupDeps) {
|
|
178
|
+
const depMeta = projectsByKey.get(dep);
|
|
179
|
+
if (depMeta)
|
|
180
|
+
knownPackages.set(depMeta.basePackage, dep);
|
|
181
|
+
}
|
|
182
|
+
const imports = await scanJavaImports(proj.repoPath, knownPackages);
|
|
183
|
+
for (const imp of imports) {
|
|
184
|
+
const providerProj = projectsByKey.get(imp.artifactKey);
|
|
185
|
+
if (!providerProj)
|
|
186
|
+
continue;
|
|
187
|
+
const qualifiedContract = `${providerProj.artifactId}::${imp.symbolName}`;
|
|
188
|
+
const dedupKey = `${proj.groupPath}→${providerProj.groupPath}::${qualifiedContract}`;
|
|
189
|
+
if (seen.has(dedupKey))
|
|
190
|
+
continue;
|
|
191
|
+
seen.add(dedupKey);
|
|
192
|
+
const link = {
|
|
193
|
+
from: providerProj.groupPath,
|
|
194
|
+
to: proj.groupPath,
|
|
195
|
+
type: 'custom',
|
|
196
|
+
contract: qualifiedContract,
|
|
197
|
+
role: 'provider',
|
|
198
|
+
};
|
|
199
|
+
links.push(link);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return { links, discoveredProjects: projectsByGroupPath };
|
|
203
|
+
}
|