@dependency-maritime/cli 0.1.0-beta.3 → 0.1.0-beta.5
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 +8 -1
- package/dist/cli/cli/graph/render-dot.d.ts +17 -1
- package/dist/cli/cli/index.d.ts +2 -1
- package/dist/cli/index.d.ts +2 -1
- package/dist/cli/index.js +66 -11
- package/dist/cli/main.js +62 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -134,9 +134,16 @@ With Graphviz `dot` installed, render without performing a second analysis:
|
|
|
134
134
|
|
|
135
135
|
```bash
|
|
136
136
|
maritime graph --input .maritime --output docs/images/dependency-graph.svg
|
|
137
|
+
maritime graph --input .maritime --output docs/images/architecture.svg \
|
|
138
|
+
--external-packages none --folder-grouping nested --edge-labels none
|
|
137
139
|
```
|
|
138
140
|
|
|
139
|
-
`.maritime/dependency-graph.json` is canonical evidence; SVG and DOT outputs are derived
|
|
141
|
+
`.maritime/dependency-graph.json` is canonical evidence; SVG (and transient DOT) outputs are derived
|
|
140
142
|
presentations. The composite Action's reproducible committed-SVG contract is limited to its pinned
|
|
141
143
|
Ubuntu Graphviz path; other runners must provide and pin `dot` themselves because layout can vary
|
|
142
144
|
between Graphviz versions.
|
|
145
|
+
|
|
146
|
+
The rendering flags are presentation-only: `--external-packages` accepts `none`, `summary`, or
|
|
147
|
+
`direct`; `--folder-grouping` accepts `none`, `top-level`, or `nested`; and `--edge-labels` accepts
|
|
148
|
+
`none` or `types`. Defaults (`direct`, `nested`, `types`) preserve existing output. The canonical
|
|
149
|
+
`.maritime/dependency-graph.json` stays complete and is never rescanned or rewritten.
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import type { MaritimeCruiseResult } from '../../schema/dependency-cruiser';
|
|
2
|
+
export declare const EXTERNAL_PACKAGE_MODES: readonly ["none", "summary", "direct"];
|
|
3
|
+
export declare const FOLDER_GROUPING_MODES: readonly ["none", "top-level", "nested"];
|
|
4
|
+
export declare const EDGE_LABEL_MODES: readonly ["none", "types"];
|
|
5
|
+
export type ExternalPackagesMode = typeof EXTERNAL_PACKAGE_MODES[number];
|
|
6
|
+
export type FolderGroupingMode = typeof FOLDER_GROUPING_MODES[number];
|
|
7
|
+
export type EdgeLabelsMode = typeof EDGE_LABEL_MODES[number];
|
|
8
|
+
export type GraphPresentationOptions = {
|
|
9
|
+
externalPackages?: ExternalPackagesMode;
|
|
10
|
+
folderGrouping?: FolderGroupingMode;
|
|
11
|
+
edgeLabels?: EdgeLabelsMode;
|
|
12
|
+
};
|
|
13
|
+
export declare const DEFAULT_GRAPH_PRESENTATION: {
|
|
14
|
+
readonly externalPackages: "direct";
|
|
15
|
+
readonly folderGrouping: "nested";
|
|
16
|
+
readonly edgeLabels: "types";
|
|
17
|
+
};
|
|
2
18
|
declare function externalPackageName(value: string): string | undefined;
|
|
3
19
|
/** Pure, deterministic conversion of a validated dependency-cruiser result to Graphviz DOT. */
|
|
4
|
-
export declare function renderDependencyGraphToDot(graph: MaritimeCruiseResult): string;
|
|
20
|
+
export declare function renderDependencyGraphToDot(graph: MaritimeCruiseResult, options?: GraphPresentationOptions): string;
|
|
5
21
|
export declare function inferGraphvizFormat(outputPath: string): 'svg' | 'dot';
|
|
6
22
|
export { externalPackageName };
|
package/dist/cli/cli/index.d.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { runValidateCommand } from './commands/validate';
|
|
|
3
3
|
import { runGraphCommand } from './commands/graph';
|
|
4
4
|
import { validateArtifacts } from './validate/validate';
|
|
5
5
|
export { runAnalyzeCommand, runValidateCommand, runGraphCommand, validateArtifacts };
|
|
6
|
-
export { renderDependencyGraphToDot, inferGraphvizFormat } from './graph/render-dot';
|
|
6
|
+
export { renderDependencyGraphToDot, inferGraphvizFormat, DEFAULT_GRAPH_PRESENTATION, EXTERNAL_PACKAGE_MODES, FOLDER_GROUPING_MODES, EDGE_LABEL_MODES } from './graph/render-dot';
|
|
7
|
+
export type { GraphPresentationOptions, ExternalPackagesMode, FolderGroupingMode, EdgeLabelsMode } from './graph/render-dot';
|
|
7
8
|
export { renderDotWithGraphviz, normalizeGraphvizSvg } from './graph/render-graphviz';
|
|
8
9
|
export * from '../schema/manifest';
|
|
9
10
|
export { readDependencyGraph, resolveDepcruiseConfig, generateDependencyGraph, runEslintComplexityScan, countLinesOfCode, writeOutputFiles } from './analyze/adapters';
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { runValidateCommand } from './commands/validate';
|
|
|
3
3
|
import { runGraphCommand } from './commands/graph';
|
|
4
4
|
import { validateArtifacts } from './validate/validate';
|
|
5
5
|
export { runAnalyzeCommand, runValidateCommand, runGraphCommand, validateArtifacts };
|
|
6
|
-
export { renderDependencyGraphToDot, inferGraphvizFormat } from './graph/render-dot';
|
|
6
|
+
export { renderDependencyGraphToDot, inferGraphvizFormat, DEFAULT_GRAPH_PRESENTATION, EXTERNAL_PACKAGE_MODES, FOLDER_GROUPING_MODES, EDGE_LABEL_MODES } from './graph/render-dot';
|
|
7
|
+
export type { GraphPresentationOptions, ExternalPackagesMode, FolderGroupingMode, EdgeLabelsMode } from './graph/render-dot';
|
|
7
8
|
export { renderDotWithGraphviz, normalizeGraphvizSvg } from './graph/render-graphviz';
|
|
8
9
|
export * from '../schema/manifest';
|
|
9
10
|
export { readDependencyGraph, resolveDepcruiseConfig, generateDependencyGraph, runEslintComplexityScan, countLinesOfCode, writeOutputFiles } from './analyze/adapters';
|
package/dist/cli/index.js
CHANGED
|
@@ -1132,6 +1132,14 @@ import * as path9 from "node:path";
|
|
|
1132
1132
|
|
|
1133
1133
|
// src/cli/graph/render-dot.ts
|
|
1134
1134
|
import * as path7 from "node:path";
|
|
1135
|
+
var EXTERNAL_PACKAGE_MODES = ["none", "summary", "direct"];
|
|
1136
|
+
var FOLDER_GROUPING_MODES = ["none", "top-level", "nested"];
|
|
1137
|
+
var EDGE_LABEL_MODES = ["none", "types"];
|
|
1138
|
+
var DEFAULT_GRAPH_PRESENTATION = {
|
|
1139
|
+
externalPackages: "direct",
|
|
1140
|
+
folderGrouping: "nested",
|
|
1141
|
+
edgeLabels: "types"
|
|
1142
|
+
};
|
|
1135
1143
|
var dotQuote = (value) => JSON.stringify(value);
|
|
1136
1144
|
var normalizedPath = (value) => value.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
1137
1145
|
function externalPackageName(value) {
|
|
@@ -1178,10 +1186,10 @@ function renderDirectory(directory, segments, indent) {
|
|
|
1178
1186
|
}
|
|
1179
1187
|
return lines;
|
|
1180
1188
|
}
|
|
1181
|
-
function edgeAttributes(dependency) {
|
|
1189
|
+
function edgeAttributes(dependency, edgeLabels) {
|
|
1182
1190
|
const attributes = [];
|
|
1183
1191
|
const dependencyTypes = [...dependency.dependencyTypes].sort();
|
|
1184
|
-
if (dependencyTypes.length > 0) attributes.push(`label=${dotQuote(dependencyTypes.join(", "))}`);
|
|
1192
|
+
if (edgeLabels === "types" && dependencyTypes.length > 0) attributes.push(`label=${dotQuote(dependencyTypes.join(", "))}`);
|
|
1185
1193
|
if (dependency.typeOnly || dependency.preCompilationOnly) attributes.push('style="dashed"');
|
|
1186
1194
|
if (dependency.circular) attributes.push('color="#d97706"', 'penwidth="2"');
|
|
1187
1195
|
if (!dependency.valid) {
|
|
@@ -1190,7 +1198,12 @@ function edgeAttributes(dependency) {
|
|
|
1190
1198
|
}
|
|
1191
1199
|
return attributes.length > 0 ? ` [${attributes.join(", ")}]` : "";
|
|
1192
1200
|
}
|
|
1193
|
-
function renderDependencyGraphToDot(graph) {
|
|
1201
|
+
function renderDependencyGraphToDot(graph, options = {}) {
|
|
1202
|
+
const presentation = {
|
|
1203
|
+
externalPackages: options.externalPackages ?? DEFAULT_GRAPH_PRESENTATION.externalPackages,
|
|
1204
|
+
folderGrouping: options.folderGrouping ?? DEFAULT_GRAPH_PRESENTATION.folderGrouping,
|
|
1205
|
+
edgeLabels: options.edgeLabels ?? DEFAULT_GRAPH_PRESENTATION.edgeLabels
|
|
1206
|
+
};
|
|
1194
1207
|
const root = { directories: /* @__PURE__ */ new Map(), files: [] };
|
|
1195
1208
|
const localSources = /* @__PURE__ */ new Set();
|
|
1196
1209
|
const externalPackages = /* @__PURE__ */ new Set();
|
|
@@ -1198,7 +1211,17 @@ function renderDependencyGraphToDot(graph) {
|
|
|
1198
1211
|
const source = normalizedPath(module.source);
|
|
1199
1212
|
if (isExternalPath(source) || module.coreModule) continue;
|
|
1200
1213
|
localSources.add(source);
|
|
1201
|
-
addLocalModule(root, source);
|
|
1214
|
+
if (presentation.folderGrouping === "nested") addLocalModule(root, source);
|
|
1215
|
+
else if (presentation.folderGrouping === "top-level") {
|
|
1216
|
+
if (!source.includes("/")) {
|
|
1217
|
+
root.files.push({ source, name: source });
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
const [topLevel] = source.split("/");
|
|
1221
|
+
const directory = root.directories.get(topLevel) ?? { directories: /* @__PURE__ */ new Map(), files: [] };
|
|
1222
|
+
root.directories.set(topLevel, directory);
|
|
1223
|
+
directory.files.push({ source, name: path7.posix.basename(source) });
|
|
1224
|
+
} else root.files.push({ source, name: path7.posix.basename(source) });
|
|
1202
1225
|
}
|
|
1203
1226
|
const edges = [];
|
|
1204
1227
|
for (const module of [...graph.modules].sort((a, b) => a.source.localeCompare(b.source))) {
|
|
@@ -1212,9 +1235,9 @@ function renderDependencyGraphToDot(graph) {
|
|
|
1212
1235
|
} else if (!dependency.coreModule) {
|
|
1213
1236
|
const looksExternal = isExternalPath(resolved) || dependency.dependencyTypes.includes("npm");
|
|
1214
1237
|
const packageName = looksExternal ? externalPackageName(isExternalPath(resolved) ? resolved : dependency.module) : void 0;
|
|
1215
|
-
if (packageName) {
|
|
1238
|
+
if (packageName && presentation.externalPackages !== "none") {
|
|
1216
1239
|
externalPackages.add(packageName);
|
|
1217
|
-
target = `external:${packageName}`;
|
|
1240
|
+
target = presentation.externalPackages === "summary" ? "external:boundary" : `external:${packageName}`;
|
|
1218
1241
|
}
|
|
1219
1242
|
}
|
|
1220
1243
|
if (target) edges.push({ from: `local:${source}`, to: target, dependency });
|
|
@@ -1223,12 +1246,18 @@ function renderDependencyGraphToDot(graph) {
|
|
|
1223
1246
|
edges.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || JSON.stringify(a.dependency).localeCompare(JSON.stringify(b.dependency)));
|
|
1224
1247
|
const lines = [
|
|
1225
1248
|
'digraph "dependency-graph" {',
|
|
1226
|
-
|
|
1249
|
+
// Graphviz 2.42 can fail init_rank when the default cluster-local ranker
|
|
1250
|
+
// encounters a large recursively nested directory hierarchy. newrank asks
|
|
1251
|
+
// dot to compute one global ranking across clusters while preserving the
|
|
1252
|
+
// cluster boxes and deterministic left-to-right presentation.
|
|
1253
|
+
' graph [compound="true", newrank="true", rankdir="LR", fontname="Helvetica"];',
|
|
1227
1254
|
' node [fontname="Helvetica", fontsize="10"];',
|
|
1228
1255
|
' edge [fontname="Helvetica", fontsize="8"];',
|
|
1229
1256
|
...renderDirectory(root, [], " ")
|
|
1230
1257
|
];
|
|
1231
|
-
if (externalPackages.size > 0) {
|
|
1258
|
+
if (presentation.externalPackages === "summary" && externalPackages.size > 0) {
|
|
1259
|
+
lines.push(' "external:boundary" [label="External packages", shape="component", style="filled,dashed", fillcolor="#e2e8f0", color="#64748b"];');
|
|
1260
|
+
} else if (presentation.externalPackages === "direct" && externalPackages.size > 0) {
|
|
1232
1261
|
lines.push(' subgraph "cluster:external-packages" {', ' label="External packages";', ' style="dashed";', ' color="#64748b";');
|
|
1233
1262
|
for (const packageName of [...externalPackages].sort()) {
|
|
1234
1263
|
lines.push(` ${dotQuote(`external:${packageName}`)} [label=${dotQuote(packageName)}, shape="component", style="filled", fillcolor="#e2e8f0"];`);
|
|
@@ -1236,7 +1265,7 @@ function renderDependencyGraphToDot(graph) {
|
|
|
1236
1265
|
lines.push(" }");
|
|
1237
1266
|
}
|
|
1238
1267
|
for (const edge of edges) {
|
|
1239
|
-
lines.push(` ${dotQuote(edge.from)} -> ${dotQuote(edge.to)}${edgeAttributes(edge.dependency)};`);
|
|
1268
|
+
lines.push(` ${dotQuote(edge.from)} -> ${dotQuote(edge.to)}${edgeAttributes(edge.dependency, presentation.edgeLabels)};`);
|
|
1240
1269
|
}
|
|
1241
1270
|
lines.push("}", "");
|
|
1242
1271
|
return lines.join("\n");
|
|
@@ -1288,7 +1317,10 @@ async function runGraphCommand(args) {
|
|
|
1288
1317
|
input: { type: "string", short: "i" },
|
|
1289
1318
|
output: { type: "string", short: "o" },
|
|
1290
1319
|
cwd: { type: "string" },
|
|
1291
|
-
help: { type: "boolean", short: "h" }
|
|
1320
|
+
help: { type: "boolean", short: "h" },
|
|
1321
|
+
"external-packages": { type: "string" },
|
|
1322
|
+
"folder-grouping": { type: "string" },
|
|
1323
|
+
"edge-labels": { type: "string" }
|
|
1292
1324
|
} }).values;
|
|
1293
1325
|
} catch (error) {
|
|
1294
1326
|
console.error(`Error parsing arguments: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -1303,6 +1335,9 @@ Renders existing canonical graph evidence without running dependency analysis.
|
|
|
1303
1335
|
Options:
|
|
1304
1336
|
-i, --input <path> .maritime directory or dependency-graph.json (default: .maritime)
|
|
1305
1337
|
-o, --output <path> SVG or DOT output path (required)
|
|
1338
|
+
--external-packages <none|summary|direct> External package presentation (default: direct)
|
|
1339
|
+
--folder-grouping <none|top-level|nested> Local folder clustering (default: nested)
|
|
1340
|
+
--edge-labels <none|types> Dependency-type labels (default: types)
|
|
1306
1341
|
--cwd <dir> Working directory root for resolution
|
|
1307
1342
|
-h, --help Show help message
|
|
1308
1343
|
`);
|
|
@@ -1312,6 +1347,22 @@ Options:
|
|
|
1312
1347
|
console.error("Error: --output is required.");
|
|
1313
1348
|
return 2;
|
|
1314
1349
|
}
|
|
1350
|
+
const validatePolicy = (flag, value, allowed, fallback) => {
|
|
1351
|
+
if (value === void 0) return fallback;
|
|
1352
|
+
if (allowed.includes(value)) return value;
|
|
1353
|
+
throw new Error(`Invalid --${flag} value "${value}". Expected one of: ${allowed.join(", ")}.`);
|
|
1354
|
+
};
|
|
1355
|
+
let externalPackages;
|
|
1356
|
+
let folderGrouping;
|
|
1357
|
+
let edgeLabels;
|
|
1358
|
+
try {
|
|
1359
|
+
externalPackages = validatePolicy("external-packages", values["external-packages"], EXTERNAL_PACKAGE_MODES, "direct");
|
|
1360
|
+
folderGrouping = validatePolicy("folder-grouping", values["folder-grouping"], FOLDER_GROUPING_MODES, "nested");
|
|
1361
|
+
edgeLabels = validatePolicy("edge-labels", values["edge-labels"], EDGE_LABEL_MODES, "types");
|
|
1362
|
+
} catch (error) {
|
|
1363
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
1364
|
+
return 2;
|
|
1365
|
+
}
|
|
1315
1366
|
const cwd = path9.resolve(values.cwd ?? process.cwd());
|
|
1316
1367
|
const input = path9.resolve(cwd, values.input ?? ".maritime");
|
|
1317
1368
|
const output = path9.resolve(cwd, values.output);
|
|
@@ -1321,7 +1372,7 @@ Options:
|
|
|
1321
1372
|
const parsed = JSON.parse(await fs4.readFile(graphPath, "utf8"));
|
|
1322
1373
|
const result = CruiseResultSchema.safeParse(parsed);
|
|
1323
1374
|
if (!result.success) throw new Error(`Invalid Maritime dependency graph: ${result.error.message}`);
|
|
1324
|
-
const dot = renderDependencyGraphToDot(result.data);
|
|
1375
|
+
const dot = renderDependencyGraphToDot(result.data, { externalPackages, folderGrouping, edgeLabels });
|
|
1325
1376
|
const format = inferGraphvizFormat(output);
|
|
1326
1377
|
await fs4.mkdir(path9.dirname(output), { recursive: true });
|
|
1327
1378
|
if (format === "dot") await fs4.writeFile(output, dot);
|
|
@@ -1380,6 +1431,10 @@ export {
|
|
|
1380
1431
|
ArtifactManifestArtifactsSchema,
|
|
1381
1432
|
ArtifactManifestSchema,
|
|
1382
1433
|
ArtifactManifestSummarySchema,
|
|
1434
|
+
DEFAULT_GRAPH_PRESENTATION,
|
|
1435
|
+
EDGE_LABEL_MODES,
|
|
1436
|
+
EXTERNAL_PACKAGE_MODES,
|
|
1437
|
+
FOLDER_GROUPING_MODES,
|
|
1383
1438
|
MANIFEST_SCHEMA_VERSION,
|
|
1384
1439
|
ValidationError,
|
|
1385
1440
|
analyzeProject,
|
package/dist/cli/main.js
CHANGED
|
@@ -1137,6 +1137,14 @@ import * as path9 from "node:path";
|
|
|
1137
1137
|
|
|
1138
1138
|
// src/cli/graph/render-dot.ts
|
|
1139
1139
|
import * as path7 from "node:path";
|
|
1140
|
+
var EXTERNAL_PACKAGE_MODES = ["none", "summary", "direct"];
|
|
1141
|
+
var FOLDER_GROUPING_MODES = ["none", "top-level", "nested"];
|
|
1142
|
+
var EDGE_LABEL_MODES = ["none", "types"];
|
|
1143
|
+
var DEFAULT_GRAPH_PRESENTATION = {
|
|
1144
|
+
externalPackages: "direct",
|
|
1145
|
+
folderGrouping: "nested",
|
|
1146
|
+
edgeLabels: "types"
|
|
1147
|
+
};
|
|
1140
1148
|
var dotQuote = (value) => JSON.stringify(value);
|
|
1141
1149
|
var normalizedPath = (value) => value.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
1142
1150
|
function externalPackageName(value) {
|
|
@@ -1183,10 +1191,10 @@ function renderDirectory(directory, segments, indent) {
|
|
|
1183
1191
|
}
|
|
1184
1192
|
return lines;
|
|
1185
1193
|
}
|
|
1186
|
-
function edgeAttributes(dependency) {
|
|
1194
|
+
function edgeAttributes(dependency, edgeLabels) {
|
|
1187
1195
|
const attributes = [];
|
|
1188
1196
|
const dependencyTypes = [...dependency.dependencyTypes].sort();
|
|
1189
|
-
if (dependencyTypes.length > 0) attributes.push(`label=${dotQuote(dependencyTypes.join(", "))}`);
|
|
1197
|
+
if (edgeLabels === "types" && dependencyTypes.length > 0) attributes.push(`label=${dotQuote(dependencyTypes.join(", "))}`);
|
|
1190
1198
|
if (dependency.typeOnly || dependency.preCompilationOnly) attributes.push('style="dashed"');
|
|
1191
1199
|
if (dependency.circular) attributes.push('color="#d97706"', 'penwidth="2"');
|
|
1192
1200
|
if (!dependency.valid) {
|
|
@@ -1195,7 +1203,12 @@ function edgeAttributes(dependency) {
|
|
|
1195
1203
|
}
|
|
1196
1204
|
return attributes.length > 0 ? ` [${attributes.join(", ")}]` : "";
|
|
1197
1205
|
}
|
|
1198
|
-
function renderDependencyGraphToDot(graph) {
|
|
1206
|
+
function renderDependencyGraphToDot(graph, options = {}) {
|
|
1207
|
+
const presentation = {
|
|
1208
|
+
externalPackages: options.externalPackages ?? DEFAULT_GRAPH_PRESENTATION.externalPackages,
|
|
1209
|
+
folderGrouping: options.folderGrouping ?? DEFAULT_GRAPH_PRESENTATION.folderGrouping,
|
|
1210
|
+
edgeLabels: options.edgeLabels ?? DEFAULT_GRAPH_PRESENTATION.edgeLabels
|
|
1211
|
+
};
|
|
1199
1212
|
const root = { directories: /* @__PURE__ */ new Map(), files: [] };
|
|
1200
1213
|
const localSources = /* @__PURE__ */ new Set();
|
|
1201
1214
|
const externalPackages = /* @__PURE__ */ new Set();
|
|
@@ -1203,7 +1216,17 @@ function renderDependencyGraphToDot(graph) {
|
|
|
1203
1216
|
const source = normalizedPath(module.source);
|
|
1204
1217
|
if (isExternalPath(source) || module.coreModule) continue;
|
|
1205
1218
|
localSources.add(source);
|
|
1206
|
-
addLocalModule(root, source);
|
|
1219
|
+
if (presentation.folderGrouping === "nested") addLocalModule(root, source);
|
|
1220
|
+
else if (presentation.folderGrouping === "top-level") {
|
|
1221
|
+
if (!source.includes("/")) {
|
|
1222
|
+
root.files.push({ source, name: source });
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
const [topLevel] = source.split("/");
|
|
1226
|
+
const directory = root.directories.get(topLevel) ?? { directories: /* @__PURE__ */ new Map(), files: [] };
|
|
1227
|
+
root.directories.set(topLevel, directory);
|
|
1228
|
+
directory.files.push({ source, name: path7.posix.basename(source) });
|
|
1229
|
+
} else root.files.push({ source, name: path7.posix.basename(source) });
|
|
1207
1230
|
}
|
|
1208
1231
|
const edges = [];
|
|
1209
1232
|
for (const module of [...graph.modules].sort((a, b) => a.source.localeCompare(b.source))) {
|
|
@@ -1217,9 +1240,9 @@ function renderDependencyGraphToDot(graph) {
|
|
|
1217
1240
|
} else if (!dependency.coreModule) {
|
|
1218
1241
|
const looksExternal = isExternalPath(resolved) || dependency.dependencyTypes.includes("npm");
|
|
1219
1242
|
const packageName = looksExternal ? externalPackageName(isExternalPath(resolved) ? resolved : dependency.module) : void 0;
|
|
1220
|
-
if (packageName) {
|
|
1243
|
+
if (packageName && presentation.externalPackages !== "none") {
|
|
1221
1244
|
externalPackages.add(packageName);
|
|
1222
|
-
target = `external:${packageName}`;
|
|
1245
|
+
target = presentation.externalPackages === "summary" ? "external:boundary" : `external:${packageName}`;
|
|
1223
1246
|
}
|
|
1224
1247
|
}
|
|
1225
1248
|
if (target) edges.push({ from: `local:${source}`, to: target, dependency });
|
|
@@ -1228,12 +1251,18 @@ function renderDependencyGraphToDot(graph) {
|
|
|
1228
1251
|
edges.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || JSON.stringify(a.dependency).localeCompare(JSON.stringify(b.dependency)));
|
|
1229
1252
|
const lines = [
|
|
1230
1253
|
'digraph "dependency-graph" {',
|
|
1231
|
-
|
|
1254
|
+
// Graphviz 2.42 can fail init_rank when the default cluster-local ranker
|
|
1255
|
+
// encounters a large recursively nested directory hierarchy. newrank asks
|
|
1256
|
+
// dot to compute one global ranking across clusters while preserving the
|
|
1257
|
+
// cluster boxes and deterministic left-to-right presentation.
|
|
1258
|
+
' graph [compound="true", newrank="true", rankdir="LR", fontname="Helvetica"];',
|
|
1232
1259
|
' node [fontname="Helvetica", fontsize="10"];',
|
|
1233
1260
|
' edge [fontname="Helvetica", fontsize="8"];',
|
|
1234
1261
|
...renderDirectory(root, [], " ")
|
|
1235
1262
|
];
|
|
1236
|
-
if (externalPackages.size > 0) {
|
|
1263
|
+
if (presentation.externalPackages === "summary" && externalPackages.size > 0) {
|
|
1264
|
+
lines.push(' "external:boundary" [label="External packages", shape="component", style="filled,dashed", fillcolor="#e2e8f0", color="#64748b"];');
|
|
1265
|
+
} else if (presentation.externalPackages === "direct" && externalPackages.size > 0) {
|
|
1237
1266
|
lines.push(' subgraph "cluster:external-packages" {', ' label="External packages";', ' style="dashed";', ' color="#64748b";');
|
|
1238
1267
|
for (const packageName of [...externalPackages].sort()) {
|
|
1239
1268
|
lines.push(` ${dotQuote(`external:${packageName}`)} [label=${dotQuote(packageName)}, shape="component", style="filled", fillcolor="#e2e8f0"];`);
|
|
@@ -1241,7 +1270,7 @@ function renderDependencyGraphToDot(graph) {
|
|
|
1241
1270
|
lines.push(" }");
|
|
1242
1271
|
}
|
|
1243
1272
|
for (const edge of edges) {
|
|
1244
|
-
lines.push(` ${dotQuote(edge.from)} -> ${dotQuote(edge.to)}${edgeAttributes(edge.dependency)};`);
|
|
1273
|
+
lines.push(` ${dotQuote(edge.from)} -> ${dotQuote(edge.to)}${edgeAttributes(edge.dependency, presentation.edgeLabels)};`);
|
|
1245
1274
|
}
|
|
1246
1275
|
lines.push("}", "");
|
|
1247
1276
|
return lines.join("\n");
|
|
@@ -1293,7 +1322,10 @@ async function runGraphCommand(args) {
|
|
|
1293
1322
|
input: { type: "string", short: "i" },
|
|
1294
1323
|
output: { type: "string", short: "o" },
|
|
1295
1324
|
cwd: { type: "string" },
|
|
1296
|
-
help: { type: "boolean", short: "h" }
|
|
1325
|
+
help: { type: "boolean", short: "h" },
|
|
1326
|
+
"external-packages": { type: "string" },
|
|
1327
|
+
"folder-grouping": { type: "string" },
|
|
1328
|
+
"edge-labels": { type: "string" }
|
|
1297
1329
|
} }).values;
|
|
1298
1330
|
} catch (error) {
|
|
1299
1331
|
console.error(`Error parsing arguments: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -1308,6 +1340,9 @@ Renders existing canonical graph evidence without running dependency analysis.
|
|
|
1308
1340
|
Options:
|
|
1309
1341
|
-i, --input <path> .maritime directory or dependency-graph.json (default: .maritime)
|
|
1310
1342
|
-o, --output <path> SVG or DOT output path (required)
|
|
1343
|
+
--external-packages <none|summary|direct> External package presentation (default: direct)
|
|
1344
|
+
--folder-grouping <none|top-level|nested> Local folder clustering (default: nested)
|
|
1345
|
+
--edge-labels <none|types> Dependency-type labels (default: types)
|
|
1311
1346
|
--cwd <dir> Working directory root for resolution
|
|
1312
1347
|
-h, --help Show help message
|
|
1313
1348
|
`);
|
|
@@ -1317,6 +1352,22 @@ Options:
|
|
|
1317
1352
|
console.error("Error: --output is required.");
|
|
1318
1353
|
return 2;
|
|
1319
1354
|
}
|
|
1355
|
+
const validatePolicy = (flag, value, allowed, fallback) => {
|
|
1356
|
+
if (value === void 0) return fallback;
|
|
1357
|
+
if (allowed.includes(value)) return value;
|
|
1358
|
+
throw new Error(`Invalid --${flag} value "${value}". Expected one of: ${allowed.join(", ")}.`);
|
|
1359
|
+
};
|
|
1360
|
+
let externalPackages;
|
|
1361
|
+
let folderGrouping;
|
|
1362
|
+
let edgeLabels;
|
|
1363
|
+
try {
|
|
1364
|
+
externalPackages = validatePolicy("external-packages", values["external-packages"], EXTERNAL_PACKAGE_MODES, "direct");
|
|
1365
|
+
folderGrouping = validatePolicy("folder-grouping", values["folder-grouping"], FOLDER_GROUPING_MODES, "nested");
|
|
1366
|
+
edgeLabels = validatePolicy("edge-labels", values["edge-labels"], EDGE_LABEL_MODES, "types");
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
1369
|
+
return 2;
|
|
1370
|
+
}
|
|
1320
1371
|
const cwd = path9.resolve(values.cwd ?? process.cwd());
|
|
1321
1372
|
const input = path9.resolve(cwd, values.input ?? ".maritime");
|
|
1322
1373
|
const output = path9.resolve(cwd, values.output);
|
|
@@ -1326,7 +1377,7 @@ Options:
|
|
|
1326
1377
|
const parsed = JSON.parse(await fs4.readFile(graphPath, "utf8"));
|
|
1327
1378
|
const result = CruiseResultSchema.safeParse(parsed);
|
|
1328
1379
|
if (!result.success) throw new Error(`Invalid Maritime dependency graph: ${result.error.message}`);
|
|
1329
|
-
const dot = renderDependencyGraphToDot(result.data);
|
|
1380
|
+
const dot = renderDependencyGraphToDot(result.data, { externalPackages, folderGrouping, edgeLabels });
|
|
1330
1381
|
const format = inferGraphvizFormat(output);
|
|
1331
1382
|
await fs4.mkdir(path9.dirname(output), { recursive: true });
|
|
1332
1383
|
if (format === "dot") await fs4.writeFile(output, dot);
|