codarch 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/client.js +5 -5
- package/index.js +2375 -1828
- package/package.json +2 -1
- package/web/assets/index-DhLrsTc_.js +9 -0
- package/web/assets/index-Dmmkr41i.css +1 -0
- package/web/demo.cc.json +6184 -0
- package/web/demo.report.json +1047 -0
- package/web/index.html +36 -0
package/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// apps/cli/src/commands/analyze.ts
|
|
4
|
-
import
|
|
4
|
+
import fs6 from "node:fs";
|
|
5
5
|
import path5 from "node:path";
|
|
6
6
|
|
|
7
7
|
// packages/parsers/src/ts/parser.ts
|
|
@@ -39,7 +39,9 @@ var TypeScriptParser = class {
|
|
|
39
39
|
}
|
|
40
40
|
parse(source, filePath) {
|
|
41
41
|
const parser = usesTsxGrammar(filePath) ? this.tsxParser : this.tsParser;
|
|
42
|
-
const root = parser.parse(source
|
|
42
|
+
const root = parser.parse(source, void 0, {
|
|
43
|
+
bufferSize: Math.max(64 * 1024, Buffer.byteLength(source, "utf8") + 1)
|
|
44
|
+
}).rootNode;
|
|
43
45
|
return {
|
|
44
46
|
metrics: measureSource(source, root),
|
|
45
47
|
imports: collectImports(root)
|
|
@@ -256,6 +258,34 @@ var MAX_RESOLVE_DEPTH = 8;
|
|
|
256
258
|
var BUILTINS = new Set(
|
|
257
259
|
builtinModules.flatMap((name) => [name, `node:${name}`])
|
|
258
260
|
);
|
|
261
|
+
var ASSET_EXTENSIONS = [
|
|
262
|
+
".css",
|
|
263
|
+
".scss",
|
|
264
|
+
".sass",
|
|
265
|
+
".less",
|
|
266
|
+
".svg",
|
|
267
|
+
".png",
|
|
268
|
+
".jpg",
|
|
269
|
+
".jpeg",
|
|
270
|
+
".gif",
|
|
271
|
+
".webp",
|
|
272
|
+
".avif",
|
|
273
|
+
".ico",
|
|
274
|
+
".woff",
|
|
275
|
+
".woff2",
|
|
276
|
+
".ttf",
|
|
277
|
+
".otf",
|
|
278
|
+
".eot",
|
|
279
|
+
".mp4",
|
|
280
|
+
".webm",
|
|
281
|
+
".mp3",
|
|
282
|
+
".wav",
|
|
283
|
+
".pdf"
|
|
284
|
+
];
|
|
285
|
+
function isAssetSpecifier(specifier) {
|
|
286
|
+
const clean = specifier.split(/[?#]/, 1)[0]?.toLowerCase() ?? "";
|
|
287
|
+
return ASSET_EXTENSIONS.some((extension) => clean.endsWith(extension));
|
|
288
|
+
}
|
|
259
289
|
var ImportResolver = class {
|
|
260
290
|
warnings = [];
|
|
261
291
|
root;
|
|
@@ -815,7 +845,7 @@ function stronglyConnectedComponents(nodes, adjacency, reverse) {
|
|
|
815
845
|
}
|
|
816
846
|
function componentLevels(components, edges, componentOf) {
|
|
817
847
|
const condensation = /* @__PURE__ */ new Map();
|
|
818
|
-
const indegree =
|
|
848
|
+
const indegree = Array.from({ length: components.length }).fill(0);
|
|
819
849
|
for (const edge of edges) {
|
|
820
850
|
const from = componentOf.get(edge.from);
|
|
821
851
|
const to = componentOf.get(edge.to);
|
|
@@ -827,7 +857,7 @@ function componentLevels(components, edges, componentOf) {
|
|
|
827
857
|
indegree[to] = (indegree[to] ?? 0) + 1;
|
|
828
858
|
}
|
|
829
859
|
}
|
|
830
|
-
const levels =
|
|
860
|
+
const levels = Array.from({ length: components.length }).fill(0);
|
|
831
861
|
const queue = [];
|
|
832
862
|
for (let index = 0; index < components.length; index++) {
|
|
833
863
|
if (indegree[index] === 0) queue.push(index);
|
|
@@ -857,22 +887,22 @@ import path4 from "node:path";
|
|
|
857
887
|
// packages/schema/src/errors.ts
|
|
858
888
|
var CcJsonError = class extends Error {
|
|
859
889
|
path;
|
|
860
|
-
constructor(message,
|
|
861
|
-
super(`${message} (en ${
|
|
890
|
+
constructor(message, path9) {
|
|
891
|
+
super(`${message} (en ${path9})`);
|
|
862
892
|
this.name = "CcJsonError";
|
|
863
|
-
this.path =
|
|
893
|
+
this.path = path9;
|
|
864
894
|
}
|
|
865
895
|
};
|
|
866
896
|
function isRecord2(value) {
|
|
867
897
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
868
898
|
}
|
|
869
|
-
function requireRecord(value,
|
|
870
|
-
if (!isRecord2(value)) throw new CcJsonError("se esperaba un objeto",
|
|
899
|
+
function requireRecord(value, path9) {
|
|
900
|
+
if (!isRecord2(value)) throw new CcJsonError("se esperaba un objeto", path9);
|
|
871
901
|
return value;
|
|
872
902
|
}
|
|
873
|
-
function requireString(value,
|
|
903
|
+
function requireString(value, path9, field5) {
|
|
874
904
|
if (typeof value !== "string" || value.length === 0) {
|
|
875
|
-
throw new CcJsonError(`"${field5}" debe ser una cadena no vac\xEDa`,
|
|
905
|
+
throw new CcJsonError(`"${field5}" debe ser una cadena no vac\xEDa`, path9);
|
|
876
906
|
}
|
|
877
907
|
return value;
|
|
878
908
|
}
|
|
@@ -883,9 +913,9 @@ var ID_LENGTH = 16;
|
|
|
883
913
|
function normalizeName(name) {
|
|
884
914
|
return name.normalize("NFC");
|
|
885
915
|
}
|
|
886
|
-
function pathSegments(
|
|
916
|
+
function pathSegments(path9) {
|
|
887
917
|
const segments = [];
|
|
888
|
-
for (const raw of
|
|
918
|
+
for (const raw of path9.replace(/\\/g, "/").split("/")) {
|
|
889
919
|
const segment = normalizeName(raw);
|
|
890
920
|
if (segment === "" || segment === ".") continue;
|
|
891
921
|
if (segment === "..") {
|
|
@@ -899,18 +929,18 @@ function pathSegments(path8) {
|
|
|
899
929
|
function canonicalPath(segments) {
|
|
900
930
|
return `/${segments.map(normalizeName).join("/")}`;
|
|
901
931
|
}
|
|
902
|
-
function idFromCanonicalPath(
|
|
903
|
-
const canonical =
|
|
932
|
+
function idFromCanonicalPath(path9, type = "File") {
|
|
933
|
+
const canonical = path9.startsWith("/") ? path9 : `/${path9}`;
|
|
904
934
|
return createHash("sha256").update(type + canonical, "utf8").digest("hex").slice(0, ID_LENGTH);
|
|
905
935
|
}
|
|
906
936
|
function nodeId(type, segments) {
|
|
907
937
|
return idFromCanonicalPath(canonicalPath(segments), type);
|
|
908
938
|
}
|
|
909
|
-
function fileId(
|
|
910
|
-
return nodeId("File", pathSegments(
|
|
939
|
+
function fileId(path9) {
|
|
940
|
+
return nodeId("File", pathSegments(path9));
|
|
911
941
|
}
|
|
912
|
-
function folderId(
|
|
913
|
-
return nodeId("Folder", pathSegments(
|
|
942
|
+
function folderId(path9) {
|
|
943
|
+
return nodeId("Folder", pathSegments(path9));
|
|
914
944
|
}
|
|
915
945
|
|
|
916
946
|
// packages/schema/src/legacy.ts
|
|
@@ -966,15 +996,15 @@ function normalizeLegacyCcJson(input) {
|
|
|
966
996
|
const metrics = {};
|
|
967
997
|
const built = [];
|
|
968
998
|
const ids = /* @__PURE__ */ new Map();
|
|
969
|
-
const buildNode = (value, parentPath,
|
|
999
|
+
const buildNode = (value, parentPath, path9, depth) => {
|
|
970
1000
|
if (depth > MAX_DEPTH) {
|
|
971
|
-
throw new CcJsonError(`profundidad m\xE1xima ${MAX_DEPTH} superada`,
|
|
1001
|
+
throw new CcJsonError(`profundidad m\xE1xima ${MAX_DEPTH} superada`, path9);
|
|
972
1002
|
}
|
|
973
|
-
const raw = requireRecord(value,
|
|
974
|
-
const name = readNodeName(raw.name, `${
|
|
1003
|
+
const raw = requireRecord(value, path9);
|
|
1004
|
+
const name = readNodeName(raw.name, `${path9}.name`);
|
|
975
1005
|
const type = raw.type;
|
|
976
1006
|
if (type !== "File" && type !== "Folder") {
|
|
977
|
-
throw new CcJsonError('"type" debe ser "File" o "Folder"', `${
|
|
1007
|
+
throw new CcJsonError('"type" debe ser "File" o "Folder"', `${path9}.type`);
|
|
978
1008
|
}
|
|
979
1009
|
const fullPath = [...parentPath, name];
|
|
980
1010
|
const id = nodeId(type, fullPath.slice(1));
|
|
@@ -982,13 +1012,13 @@ function normalizeLegacyCcJson(input) {
|
|
|
982
1012
|
if (previous !== void 0) {
|
|
983
1013
|
throw new CcJsonError(
|
|
984
1014
|
`id duplicado "${id}" para "${previous}" y "${fullPath.join("/")}"`,
|
|
985
|
-
`${
|
|
1015
|
+
`${path9}.name`
|
|
986
1016
|
);
|
|
987
1017
|
}
|
|
988
1018
|
ids.set(id, fullPath.join("/"));
|
|
989
1019
|
built.push({ id, name, fullPath: fullPath.join("/") });
|
|
990
1020
|
const node = { id, name, type };
|
|
991
|
-
const attributes = readMetrics(raw.attributes, `${
|
|
1021
|
+
const attributes = readMetrics(raw.attributes, `${path9}.attributes`);
|
|
992
1022
|
if (attributes) metrics[id] = attributes;
|
|
993
1023
|
const contentHash = readOptionalString(raw.checksum) ?? readOptionalString(raw.contentHash);
|
|
994
1024
|
if (contentHash) node.contentHash = contentHash;
|
|
@@ -996,14 +1026,14 @@ function normalizeLegacyCcJson(input) {
|
|
|
996
1026
|
if (link) node.link = link;
|
|
997
1027
|
if (raw.children !== void 0) {
|
|
998
1028
|
if (!Array.isArray(raw.children)) {
|
|
999
|
-
throw new CcJsonError('"children" debe ser un array', `${
|
|
1029
|
+
throw new CcJsonError('"children" debe ser un array', `${path9}.children`);
|
|
1000
1030
|
}
|
|
1001
1031
|
if (type === "File" && raw.children.length > 0) {
|
|
1002
|
-
throw new CcJsonError("un fichero no puede tener hijos", `${
|
|
1032
|
+
throw new CcJsonError("un fichero no puede tener hijos", `${path9}.children`);
|
|
1003
1033
|
}
|
|
1004
1034
|
if (type === "Folder" && raw.children.length > 0) {
|
|
1005
1035
|
node.children = raw.children.map(
|
|
1006
|
-
(child, index) => buildNode(child, fullPath, `${
|
|
1036
|
+
(child, index) => buildNode(child, fullPath, `${path9}.children[${index}]`, depth + 1)
|
|
1007
1037
|
);
|
|
1008
1038
|
}
|
|
1009
1039
|
}
|
|
@@ -1020,15 +1050,15 @@ function normalizeLegacyCcJson(input) {
|
|
|
1020
1050
|
throw new CcJsonError('"edges" debe ser un array', `${base}.edges`);
|
|
1021
1051
|
}
|
|
1022
1052
|
edgesRaw.forEach((edge, index) => {
|
|
1023
|
-
const
|
|
1024
|
-
const raw = requireRecord(edge,
|
|
1025
|
-
const fromName = requireString(raw.fromNodeName, `${
|
|
1026
|
-
const toName = requireString(raw.toNodeName, `${
|
|
1053
|
+
const path9 = `${base}.edges[${index}]`;
|
|
1054
|
+
const raw = requireRecord(edge, path9);
|
|
1055
|
+
const fromName = requireString(raw.fromNodeName, `${path9}.fromNodeName`, "fromNodeName");
|
|
1056
|
+
const toName = requireString(raw.toNodeName, `${path9}.toNodeName`, "toNodeName");
|
|
1027
1057
|
const normalized = {
|
|
1028
|
-
fromId: resolveEndpoint(fromName, pathIndex, stripNames, `${
|
|
1029
|
-
toId: resolveEndpoint(toName, pathIndex, stripNames, `${
|
|
1058
|
+
fromId: resolveEndpoint(fromName, pathIndex, stripNames, `${path9}.fromNodeName`),
|
|
1059
|
+
toId: resolveEndpoint(toName, pathIndex, stripNames, `${path9}.toNodeName`)
|
|
1030
1060
|
};
|
|
1031
|
-
const attributes = readMetrics(raw.attributes, `${
|
|
1061
|
+
const attributes = readMetrics(raw.attributes, `${path9}.attributes`);
|
|
1032
1062
|
if (attributes) normalized.attributes = attributes;
|
|
1033
1063
|
if (typeof raw.isCyclic === "boolean") normalized.isCyclic = raw.isCyclic;
|
|
1034
1064
|
if (typeof raw.isPointingUpwards === "boolean") {
|
|
@@ -1113,10 +1143,10 @@ function buildPathIndex(built, multipleRoots) {
|
|
|
1113
1143
|
}
|
|
1114
1144
|
return index;
|
|
1115
1145
|
}
|
|
1116
|
-
function resolveEndpoint(endpoint, index, stripNames,
|
|
1146
|
+
function resolveEndpoint(endpoint, index, stripNames, path9) {
|
|
1117
1147
|
const segments = canonicalSegments(endpoint);
|
|
1118
1148
|
if (segments.length === 0) {
|
|
1119
|
-
throw new CcJsonError("extremo de arista vac\xEDo",
|
|
1149
|
+
throw new CcJsonError("extremo de arista vac\xEDo", path9);
|
|
1120
1150
|
}
|
|
1121
1151
|
const candidates = [segments.join("/")];
|
|
1122
1152
|
if (segments.length > 1 && stripNames.has(segments[0])) {
|
|
@@ -1128,7 +1158,7 @@ function resolveEndpoint(endpoint, index, stripNames, path8) {
|
|
|
1128
1158
|
}
|
|
1129
1159
|
const bare = index.get(segments[segments.length - 1]);
|
|
1130
1160
|
if (bare) return bare;
|
|
1131
|
-
throw new CcJsonError(`no se pudo resolver el nodo "${endpoint}"`,
|
|
1161
|
+
throw new CcJsonError(`no se pudo resolver el nodo "${endpoint}"`, path9);
|
|
1132
1162
|
}
|
|
1133
1163
|
function canonicalSegments(value) {
|
|
1134
1164
|
const result = [];
|
|
@@ -1143,85 +1173,85 @@ function canonicalSegments(value) {
|
|
|
1143
1173
|
}
|
|
1144
1174
|
return result;
|
|
1145
1175
|
}
|
|
1146
|
-
function readNodeName(value,
|
|
1147
|
-
const name = normalizeName(requireString(value,
|
|
1176
|
+
function readNodeName(value, path9) {
|
|
1177
|
+
const name = normalizeName(requireString(value, path9, "name"));
|
|
1148
1178
|
if (name === "." || name === "..") {
|
|
1149
|
-
throw new CcJsonError('el nombre no puede ser "." ni ".."',
|
|
1179
|
+
throw new CcJsonError('el nombre no puede ser "." ni ".."', path9);
|
|
1150
1180
|
}
|
|
1151
1181
|
if (name.includes("/")) {
|
|
1152
|
-
throw new CcJsonError('el nombre no puede contener "/"',
|
|
1182
|
+
throw new CcJsonError('el nombre no puede contener "/"', path9);
|
|
1153
1183
|
}
|
|
1154
1184
|
return name;
|
|
1155
1185
|
}
|
|
1156
1186
|
function readOptionalString(value) {
|
|
1157
1187
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1158
1188
|
}
|
|
1159
|
-
function readMetrics(value,
|
|
1189
|
+
function readMetrics(value, path9) {
|
|
1160
1190
|
if (value === void 0) return void 0;
|
|
1161
|
-
const raw = requireRecord(value,
|
|
1191
|
+
const raw = requireRecord(value, path9);
|
|
1162
1192
|
const out = {};
|
|
1163
1193
|
for (const [metric, metricValue] of Object.entries(raw)) {
|
|
1164
1194
|
if (typeof metricValue !== "number" || !Number.isFinite(metricValue)) {
|
|
1165
1195
|
throw new CcJsonError(
|
|
1166
1196
|
`la m\xE9trica "${metric}" debe ser un n\xFAmero finito`,
|
|
1167
|
-
`${
|
|
1197
|
+
`${path9}["${metric}"]`
|
|
1168
1198
|
);
|
|
1169
1199
|
}
|
|
1170
1200
|
out[metric] = metricValue;
|
|
1171
1201
|
}
|
|
1172
1202
|
return Object.keys(out).length > 0 ? out : void 0;
|
|
1173
1203
|
}
|
|
1174
|
-
function readAttributeTypes(value,
|
|
1204
|
+
function readAttributeTypes(value, path9) {
|
|
1175
1205
|
if (value === void 0) return {};
|
|
1176
|
-
const raw = requireRecord(value,
|
|
1206
|
+
const raw = requireRecord(value, path9);
|
|
1177
1207
|
if ("nodes" in raw || "edges" in raw) {
|
|
1178
|
-
const nodes2 = raw.nodes === void 0 ? void 0 : readTypeMap(raw.nodes, `${
|
|
1179
|
-
const edges = raw.edges === void 0 ? void 0 : readTypeMap(raw.edges, `${
|
|
1208
|
+
const nodes2 = raw.nodes === void 0 ? void 0 : readTypeMap(raw.nodes, `${path9}.nodes`);
|
|
1209
|
+
const edges = raw.edges === void 0 ? void 0 : readTypeMap(raw.edges, `${path9}.edges`);
|
|
1180
1210
|
return {
|
|
1181
1211
|
...nodes2 && Object.keys(nodes2).length > 0 ? { nodes: nodes2 } : {},
|
|
1182
1212
|
...edges && Object.keys(edges).length > 0 ? { edges } : {}
|
|
1183
1213
|
};
|
|
1184
1214
|
}
|
|
1185
|
-
const nodes = readTypeMap(raw,
|
|
1215
|
+
const nodes = readTypeMap(raw, path9);
|
|
1186
1216
|
return Object.keys(nodes).length > 0 ? { nodes } : {};
|
|
1187
1217
|
}
|
|
1188
|
-
function readTypeMap(value,
|
|
1189
|
-
const raw = requireRecord(value,
|
|
1218
|
+
function readTypeMap(value, path9) {
|
|
1219
|
+
const raw = requireRecord(value, path9);
|
|
1190
1220
|
const out = {};
|
|
1191
1221
|
for (const [metric, type] of Object.entries(raw)) {
|
|
1192
1222
|
if (type !== "absolute" && type !== "relative") {
|
|
1193
1223
|
throw new CcJsonError(
|
|
1194
1224
|
`el tipo de "${metric}" debe ser "absolute" o "relative"`,
|
|
1195
|
-
`${
|
|
1225
|
+
`${path9}["${metric}"]`
|
|
1196
1226
|
);
|
|
1197
1227
|
}
|
|
1198
1228
|
out[metric] = type;
|
|
1199
1229
|
}
|
|
1200
1230
|
return out;
|
|
1201
1231
|
}
|
|
1202
|
-
function readDescriptors(value,
|
|
1232
|
+
function readDescriptors(value, path9) {
|
|
1203
1233
|
if (value === void 0) return {};
|
|
1204
|
-
const raw = requireRecord(value,
|
|
1234
|
+
const raw = requireRecord(value, path9);
|
|
1205
1235
|
if ("nodes" in raw || "edges" in raw) {
|
|
1206
|
-
const nodes = raw.nodes === void 0 ? void 0 : readDescriptorMap(raw.nodes, `${
|
|
1207
|
-
const edges = raw.edges === void 0 ? void 0 : readDescriptorMap(raw.edges, `${
|
|
1236
|
+
const nodes = raw.nodes === void 0 ? void 0 : readDescriptorMap(raw.nodes, `${path9}.nodes`);
|
|
1237
|
+
const edges = raw.edges === void 0 ? void 0 : readDescriptorMap(raw.edges, `${path9}.edges`);
|
|
1208
1238
|
return {
|
|
1209
1239
|
...nodes && Object.keys(nodes).length > 0 ? { nodes } : {},
|
|
1210
1240
|
...edges && Object.keys(edges).length > 0 ? { edges } : {}
|
|
1211
1241
|
};
|
|
1212
1242
|
}
|
|
1213
|
-
const flat = readDescriptorMap(raw,
|
|
1243
|
+
const flat = readDescriptorMap(raw, path9);
|
|
1214
1244
|
return Object.keys(flat).length > 0 ? { flat } : {};
|
|
1215
1245
|
}
|
|
1216
|
-
function readDescriptorMap(value,
|
|
1217
|
-
const raw = requireRecord(value,
|
|
1246
|
+
function readDescriptorMap(value, path9) {
|
|
1247
|
+
const raw = requireRecord(value, path9);
|
|
1218
1248
|
const out = {};
|
|
1219
1249
|
for (const [metric, descriptor] of Object.entries(raw)) {
|
|
1220
|
-
const record = requireRecord(descriptor, `${
|
|
1250
|
+
const record = requireRecord(descriptor, `${path9}["${metric}"]`);
|
|
1221
1251
|
if (record.direction !== void 0 && typeof record.direction !== "number") {
|
|
1222
1252
|
throw new CcJsonError(
|
|
1223
1253
|
`"direction" de "${metric}" debe ser un n\xFAmero`,
|
|
1224
|
-
`${
|
|
1254
|
+
`${path9}["${metric}"].direction`
|
|
1225
1255
|
);
|
|
1226
1256
|
}
|
|
1227
1257
|
out[metric] = record;
|
|
@@ -1229,11 +1259,10 @@ function readDescriptorMap(value, path8) {
|
|
|
1229
1259
|
return out;
|
|
1230
1260
|
}
|
|
1231
1261
|
|
|
1232
|
-
// packages/schema/src/
|
|
1262
|
+
// packages/schema/src/parse2.ts
|
|
1233
1263
|
var MAX_DEPTH2 = 512;
|
|
1234
|
-
function
|
|
1264
|
+
function parseCcJson2(input) {
|
|
1235
1265
|
if (!isRecord2(input)) throw new CcJsonError("se esperaba un objeto", "$");
|
|
1236
|
-
if (isLegacyCcJson(input)) return normalizeLegacyCcJson(input);
|
|
1237
1266
|
const metaRaw = requireRecord(input.meta, "$.meta");
|
|
1238
1267
|
const apiVersion = requireString(metaRaw.apiVersion, "$.meta.apiVersion", "apiVersion");
|
|
1239
1268
|
if (!apiVersion.startsWith("2.")) {
|
|
@@ -1247,26 +1276,26 @@ function parseCcJson(input) {
|
|
|
1247
1276
|
const roots = Array.isArray(filesRaw) ? filesRaw : [filesRaw];
|
|
1248
1277
|
if (roots.length === 0) throw new CcJsonError("el \xE1rbol est\xE1 vac\xEDo", "$.files");
|
|
1249
1278
|
const ids = /* @__PURE__ */ new Set();
|
|
1250
|
-
const validateNode = (value,
|
|
1251
|
-
if (depth > MAX_DEPTH2) throw new CcJsonError(`profundidad m\xE1xima ${MAX_DEPTH2} superada`,
|
|
1252
|
-
const raw = requireRecord(value,
|
|
1253
|
-
const id = requireString(raw.id, `${
|
|
1254
|
-
if (ids.has(id)) throw new CcJsonError(`id duplicado "${id}"`, `${
|
|
1279
|
+
const validateNode = (value, path9, depth) => {
|
|
1280
|
+
if (depth > MAX_DEPTH2) throw new CcJsonError(`profundidad m\xE1xima ${MAX_DEPTH2} superada`, path9);
|
|
1281
|
+
const raw = requireRecord(value, path9);
|
|
1282
|
+
const id = requireString(raw.id, `${path9}.id`, "id");
|
|
1283
|
+
if (ids.has(id)) throw new CcJsonError(`id duplicado "${id}"`, `${path9}.id`);
|
|
1255
1284
|
ids.add(id);
|
|
1256
|
-
const name = requireString(raw.name, `${
|
|
1285
|
+
const name = requireString(raw.name, `${path9}.name`, "name");
|
|
1257
1286
|
const type = raw.type;
|
|
1258
1287
|
if (type !== "File" && type !== "Folder") {
|
|
1259
|
-
throw new CcJsonError('"type" debe ser "File" o "Folder"', `${
|
|
1288
|
+
throw new CcJsonError('"type" debe ser "File" o "Folder"', `${path9}.type`);
|
|
1260
1289
|
}
|
|
1261
1290
|
const node = { id, name, type };
|
|
1262
1291
|
if (typeof raw.contentHash === "string") node.contentHash = raw.contentHash;
|
|
1263
1292
|
if (typeof raw.link === "string") node.link = raw.link;
|
|
1264
1293
|
if (raw.children !== void 0) {
|
|
1265
1294
|
if (!Array.isArray(raw.children)) {
|
|
1266
|
-
throw new CcJsonError('"children" debe ser un array', `${
|
|
1295
|
+
throw new CcJsonError('"children" debe ser un array', `${path9}.children`);
|
|
1267
1296
|
}
|
|
1268
1297
|
node.children = raw.children.map(
|
|
1269
|
-
(child, index) => validateNode(child, `${
|
|
1298
|
+
(child, index) => validateNode(child, `${path9}.children[${index}]`, depth + 1)
|
|
1270
1299
|
);
|
|
1271
1300
|
}
|
|
1272
1301
|
return node;
|
|
@@ -1297,10 +1326,10 @@ function parseCcJson(input) {
|
|
|
1297
1326
|
throw new CcJsonError('"edges" debe ser un array', "$.lenses.dependency.edges");
|
|
1298
1327
|
}
|
|
1299
1328
|
dependency.edges.forEach((edge, index) => {
|
|
1300
|
-
const
|
|
1301
|
-
const raw = requireRecord(edge,
|
|
1302
|
-
requireString(raw.fromId, `${
|
|
1303
|
-
requireString(raw.toId, `${
|
|
1329
|
+
const path9 = `$.lenses.dependency.edges[${index}]`;
|
|
1330
|
+
const raw = requireRecord(edge, path9);
|
|
1331
|
+
requireString(raw.fromId, `${path9}.fromId`, "fromId");
|
|
1332
|
+
requireString(raw.toId, `${path9}.toId`, "toId");
|
|
1304
1333
|
});
|
|
1305
1334
|
}
|
|
1306
1335
|
}
|
|
@@ -1313,6 +1342,12 @@ function parseCcJson(input) {
|
|
|
1313
1342
|
};
|
|
1314
1343
|
}
|
|
1315
1344
|
|
|
1345
|
+
// packages/schema/src/parse.ts
|
|
1346
|
+
function parseCcJson(input) {
|
|
1347
|
+
if (isLegacyCcJson(input)) return normalizeLegacyCcJson(input);
|
|
1348
|
+
return parseCcJson2(input);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1316
1351
|
// packages/schema/src/tree.ts
|
|
1317
1352
|
function walkTree(roots, visit) {
|
|
1318
1353
|
const stack = roots.map((node) => ({ node, path: node.name, depth: 0 })).reverse();
|
|
@@ -1383,9 +1418,9 @@ function topByMetric(cc, metric, limit = 10) {
|
|
|
1383
1418
|
const ranked = [];
|
|
1384
1419
|
for (const [id, values] of Object.entries(attributes)) {
|
|
1385
1420
|
const value = values[metric];
|
|
1386
|
-
const
|
|
1387
|
-
if (value === void 0 ||
|
|
1388
|
-
ranked.push({ id, path:
|
|
1421
|
+
const path9 = paths.get(id);
|
|
1422
|
+
if (value === void 0 || path9 === void 0) continue;
|
|
1423
|
+
ranked.push({ id, path: path9, value });
|
|
1389
1424
|
}
|
|
1390
1425
|
ranked.sort((a, b) => b.value - a.value || a.path.localeCompare(b.path));
|
|
1391
1426
|
return ranked.slice(0, limit);
|
|
@@ -1954,8 +1989,8 @@ function buildSnapshot(input) {
|
|
|
1954
1989
|
isCyclic: edge.isCyclic
|
|
1955
1990
|
}));
|
|
1956
1991
|
const dependencyNodes = {};
|
|
1957
|
-
for (const [
|
|
1958
|
-
dependencyNodes[fileId(
|
|
1992
|
+
for (const [path9, metrics] of input.graph.metrics) {
|
|
1993
|
+
dependencyNodes[fileId(path9)] = { level: metrics.dependencyLevel };
|
|
1959
1994
|
}
|
|
1960
1995
|
const migrationLens = input.migration ? migrationLensFrom(input.migration) : void 0;
|
|
1961
1996
|
const lenses = {
|
|
@@ -2219,6 +2254,7 @@ function analyzeRepository(rootInput, options = {}) {
|
|
|
2219
2254
|
let fileUnresolvedImports = 0;
|
|
2220
2255
|
for (const imported of parsed.imports) {
|
|
2221
2256
|
if (imported.typeOnly) fileTypeOnlyImports++;
|
|
2257
|
+
if (isAssetSpecifier(imported.specifier)) continue;
|
|
2222
2258
|
const result = resolver.resolve(file.absolutePath, imported.specifier);
|
|
2223
2259
|
if (result.kind === "internal") {
|
|
2224
2260
|
const targetLanguage = languageForPath(result.file.relativePath);
|
|
@@ -2369,1900 +2405,1902 @@ function readProjectName(root) {
|
|
|
2369
2405
|
return path4.basename(root) || "Proyecto";
|
|
2370
2406
|
}
|
|
2371
2407
|
|
|
2372
|
-
// packages/metrics/src/
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2408
|
+
// packages/metrics/src/risk.ts
|
|
2409
|
+
var COMPLEXITY_METRIC_CANDIDATES = [
|
|
2410
|
+
"maxComplexity",
|
|
2411
|
+
"max_complexity_per_function",
|
|
2412
|
+
"complexity",
|
|
2413
|
+
"logic_complexity",
|
|
2414
|
+
"sonar_cognitive_complexity"
|
|
2415
|
+
];
|
|
2416
|
+
var CHURN_METRIC_CANDIDATES = [
|
|
2417
|
+
"number_of_commits",
|
|
2418
|
+
"number_of_authors",
|
|
2419
|
+
"number_of_renames"
|
|
2420
|
+
];
|
|
2421
|
+
var RISK_WEIGHTS = {
|
|
2422
|
+
complexity: 0.5,
|
|
2423
|
+
churn: 0.32,
|
|
2424
|
+
size: 0.18
|
|
2425
|
+
};
|
|
2426
|
+
var RISK_GAMMA = 1.7;
|
|
2427
|
+
function riskScores(files) {
|
|
2428
|
+
const complexityP95 = percentile(
|
|
2429
|
+
files.map((file) => file.complexity).filter((value) => value > 0),
|
|
2430
|
+
0.95
|
|
2431
|
+
);
|
|
2432
|
+
const churnP95 = percentile(
|
|
2433
|
+
files.map((file) => file.churn).filter((value) => value > 0),
|
|
2434
|
+
0.95
|
|
2435
|
+
);
|
|
2436
|
+
const sizeP95 = percentile(
|
|
2437
|
+
files.map((file) => file.size).filter((value) => value > 0),
|
|
2438
|
+
0.95
|
|
2439
|
+
);
|
|
2440
|
+
const complexityWeight = complexityP95 > 0 ? RISK_WEIGHTS.complexity : 0;
|
|
2441
|
+
const churnWeight = churnP95 > 0 ? RISK_WEIGHTS.churn : 0;
|
|
2442
|
+
const sizeWeight = sizeP95 > 0 ? RISK_WEIGHTS.size : 0;
|
|
2443
|
+
const totalWeight = complexityWeight + churnWeight + sizeWeight;
|
|
2444
|
+
if (totalWeight === 0) return files.map(() => 0);
|
|
2445
|
+
return files.map((file) => {
|
|
2446
|
+
const raw = (complexityWeight * norm(file.complexity, complexityP95) + churnWeight * norm(file.churn, churnP95) + sizeWeight * norm(file.size, sizeP95)) / totalWeight;
|
|
2447
|
+
return Math.pow(clamp01(raw), RISK_GAMMA);
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
function firstMetric(available, candidates) {
|
|
2451
|
+
const set = available instanceof Set ? available : new Set(available);
|
|
2452
|
+
for (const candidate of candidates) {
|
|
2453
|
+
if (set.has(candidate)) return candidate;
|
|
2454
|
+
}
|
|
2455
|
+
return null;
|
|
2456
|
+
}
|
|
2457
|
+
function norm(value, p95) {
|
|
2458
|
+
return p95 > 0 ? clamp01(value / p95) : 0;
|
|
2459
|
+
}
|
|
2460
|
+
function percentile(values, p) {
|
|
2461
|
+
if (values.length === 0) return 0;
|
|
2462
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
2463
|
+
const index = Math.min(
|
|
2464
|
+
sorted.length - 1,
|
|
2465
|
+
Math.max(0, Math.floor((sorted.length - 1) * p))
|
|
2466
|
+
);
|
|
2467
|
+
return sorted[index] ?? 0;
|
|
2468
|
+
}
|
|
2469
|
+
function clamp01(value) {
|
|
2470
|
+
if (!Number.isFinite(value)) return 0;
|
|
2471
|
+
return Math.max(0, Math.min(1, value));
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
// packages/render/src/model.ts
|
|
2475
|
+
var RenderError = class extends Error {
|
|
2376
2476
|
constructor(message) {
|
|
2377
2477
|
super(message);
|
|
2378
|
-
this.name = "
|
|
2478
|
+
this.name = "RenderError";
|
|
2379
2479
|
}
|
|
2380
2480
|
};
|
|
2381
|
-
var
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2481
|
+
var PALETTE = [
|
|
2482
|
+
"#b9c4d0",
|
|
2483
|
+
"#c9b8a8",
|
|
2484
|
+
"#a9bfae",
|
|
2485
|
+
"#c4b6c9",
|
|
2486
|
+
"#c9c2a6",
|
|
2487
|
+
"#d5d2ca",
|
|
2488
|
+
"#c2c8d0",
|
|
2489
|
+
"#d6c6bd",
|
|
2490
|
+
"#b7c6c2",
|
|
2491
|
+
"#cbbfb0",
|
|
2492
|
+
"#bcc3b9",
|
|
2493
|
+
"#cfc6c9"
|
|
2494
|
+
];
|
|
2495
|
+
var CURATED_METRICS = [
|
|
2496
|
+
"rloc",
|
|
2497
|
+
"maxComplexity",
|
|
2498
|
+
"max_complexity_per_function",
|
|
2499
|
+
"complexity",
|
|
2500
|
+
"functions",
|
|
2501
|
+
"number_of_functions",
|
|
2502
|
+
"maxFunctionLength",
|
|
2503
|
+
"loc",
|
|
2504
|
+
"imports",
|
|
2505
|
+
"externalImports",
|
|
2506
|
+
"builtinImports",
|
|
2507
|
+
"typeOnlyImports",
|
|
2508
|
+
"unresolvedImports",
|
|
2509
|
+
"fanIn",
|
|
2510
|
+
"fanOut",
|
|
2511
|
+
"inCycle",
|
|
2512
|
+
"cycleSize",
|
|
2513
|
+
"dependencyLevel",
|
|
2514
|
+
"number_of_commits",
|
|
2515
|
+
"number_of_authors",
|
|
2516
|
+
"incoming_dependencies",
|
|
2517
|
+
"outgoing_dependencies"
|
|
2518
|
+
];
|
|
2519
|
+
function isRecord3(value) {
|
|
2520
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2391
2521
|
}
|
|
2392
|
-
function
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2522
|
+
function valueOf(values, key) {
|
|
2523
|
+
if (!key || !values) return void 0;
|
|
2524
|
+
const value = values[key];
|
|
2525
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2526
|
+
}
|
|
2527
|
+
function percentile2(values, p) {
|
|
2528
|
+
if (values.length === 0) return 0;
|
|
2529
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
2530
|
+
const index = Math.min(
|
|
2531
|
+
sorted.length - 1,
|
|
2532
|
+
Math.max(0, Math.floor((sorted.length - 1) * p))
|
|
2533
|
+
);
|
|
2534
|
+
return sorted[index] ?? 0;
|
|
2535
|
+
}
|
|
2536
|
+
function hashString(value) {
|
|
2537
|
+
let hash = 2166136261;
|
|
2538
|
+
for (let index = 0; index < value.length; index++) {
|
|
2539
|
+
hash ^= value.charCodeAt(index);
|
|
2540
|
+
hash = Math.imul(hash, 16777619);
|
|
2400
2541
|
}
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2542
|
+
return hash >>> 0;
|
|
2543
|
+
}
|
|
2544
|
+
function sectionForPath(path9) {
|
|
2545
|
+
const folders = path9.split("/").filter(Boolean).slice(0, -1);
|
|
2546
|
+
if (folders.length === 0) return "(ra\xEDz)";
|
|
2547
|
+
if (folders.length === 1) return folders[0];
|
|
2548
|
+
return folders.slice(0, 3).join("/");
|
|
2549
|
+
}
|
|
2550
|
+
function asEdges(value) {
|
|
2551
|
+
if (!Array.isArray(value)) return [];
|
|
2552
|
+
const edges = [];
|
|
2553
|
+
for (const item of value) {
|
|
2554
|
+
if (!isRecord3(item)) continue;
|
|
2555
|
+
if (typeof item.fromId !== "string" || typeof item.toId !== "string") continue;
|
|
2556
|
+
edges.push({
|
|
2557
|
+
fromId: item.fromId,
|
|
2558
|
+
toId: item.toId,
|
|
2559
|
+
...item.isCyclic === true ? { isCyclic: true } : {}
|
|
2560
|
+
});
|
|
2408
2561
|
}
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
throw error;
|
|
2562
|
+
return edges;
|
|
2563
|
+
}
|
|
2564
|
+
function metricTitle(attributes, key) {
|
|
2565
|
+
const descriptor = attributes?.[key];
|
|
2566
|
+
if (isRecord3(descriptor) && typeof descriptor.title === "string" && descriptor.title) {
|
|
2567
|
+
return descriptor.title;
|
|
2416
2568
|
}
|
|
2569
|
+
return key;
|
|
2417
2570
|
}
|
|
2418
|
-
function
|
|
2419
|
-
|
|
2420
|
-
if (
|
|
2421
|
-
|
|
2571
|
+
function metricDirection(attributes, key) {
|
|
2572
|
+
const descriptor = attributes?.[key];
|
|
2573
|
+
if (isRecord3(descriptor) && typeof descriptor.direction === "number") {
|
|
2574
|
+
return descriptor.direction;
|
|
2422
2575
|
}
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
if (
|
|
2433
|
-
throw new
|
|
2434
|
-
`
|
|
2435
|
-
);
|
|
2436
|
-
}
|
|
2437
|
-
const rawPatterns = layer.patterns ?? layer.paths;
|
|
2438
|
-
if (!Array.isArray(rawPatterns) || rawPatterns.length === 0) {
|
|
2439
|
-
throw new ConfigError(
|
|
2440
|
-
`config.layers[${index}].patterns debe ser una lista no vac\xEDa`
|
|
2576
|
+
return 0;
|
|
2577
|
+
}
|
|
2578
|
+
function buildRenderModel(cc, options = {}) {
|
|
2579
|
+
const stats = getStats(cc);
|
|
2580
|
+
const coverage = new Set(stats.metricCoverage.map((entry) => entry.name));
|
|
2581
|
+
const attributes = cc.lenses?.metrics?.attributes ?? {};
|
|
2582
|
+
const descriptors = cc.lenses?.metrics?.attributeDescriptors;
|
|
2583
|
+
let areaMetric;
|
|
2584
|
+
if (options.areaMetric) {
|
|
2585
|
+
if (!coverage.has(options.areaMetric)) {
|
|
2586
|
+
throw new RenderError(
|
|
2587
|
+
`la m\xE9trica de \xE1rea "${options.areaMetric}" no existe en este cc.json`
|
|
2441
2588
|
);
|
|
2442
2589
|
}
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
`config.layers[${index}].patterns[${patternIndex}]`
|
|
2447
|
-
).replaceAll("\\", "/").replace(/^\.\//, "");
|
|
2448
|
-
if (pattern.startsWith("/") || /^[A-Za-z]:\//.test(pattern)) {
|
|
2449
|
-
throw new ConfigError(
|
|
2450
|
-
`el patr\xF3n "${pattern}" debe ser relativo al root del proyecto`
|
|
2451
|
-
);
|
|
2452
|
-
}
|
|
2453
|
-
return pattern;
|
|
2454
|
-
});
|
|
2455
|
-
layers.push({ name, patterns });
|
|
2456
|
-
});
|
|
2457
|
-
return layers;
|
|
2458
|
-
}
|
|
2459
|
-
function parseRules(value, layerNames) {
|
|
2460
|
-
if (value === void 0) return [];
|
|
2461
|
-
if (!Array.isArray(value)) {
|
|
2462
|
-
throw new ConfigError("config.rules debe ser una lista");
|
|
2590
|
+
areaMetric = options.areaMetric;
|
|
2591
|
+
} else {
|
|
2592
|
+
areaMetric = defaultMetric(stats) ?? null;
|
|
2463
2593
|
}
|
|
2464
|
-
const
|
|
2465
|
-
const
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2594
|
+
const complexityKey = firstMetric(coverage, COMPLEXITY_METRIC_CANDIDATES);
|
|
2595
|
+
const churnKey = firstMetric(coverage, CHURN_METRIC_CANDIDATES);
|
|
2596
|
+
const metricKeys = [];
|
|
2597
|
+
const addMetric = (key) => {
|
|
2598
|
+
if (key && coverage.has(key) && !metricKeys.includes(key)) metricKeys.push(key);
|
|
2599
|
+
};
|
|
2600
|
+
for (const key of CURATED_METRICS) addMetric(key);
|
|
2601
|
+
addMetric(areaMetric);
|
|
2602
|
+
addMetric(complexityKey);
|
|
2603
|
+
addMetric(churnKey);
|
|
2604
|
+
if (options.color && options.color !== "risk" && options.color !== "structure") {
|
|
2605
|
+
addMetric(options.color);
|
|
2606
|
+
}
|
|
2607
|
+
const singleRoot = cc.files.length === 1 && cc.files[0]?.type === "Folder" ? cc.files[0] : null;
|
|
2608
|
+
const prefix = singleRoot ? `${singleRoot.name}/` : "";
|
|
2609
|
+
const rootName = singleRoot?.name ?? "";
|
|
2610
|
+
const warnings = [];
|
|
2611
|
+
const files = [];
|
|
2612
|
+
const fileIndexById = /* @__PURE__ */ new Map();
|
|
2613
|
+
walkFiles(cc.files, (entry) => {
|
|
2614
|
+
const fullPath = entry.path;
|
|
2615
|
+
const path9 = prefix && fullPath.startsWith(prefix) ? fullPath.slice(prefix.length) : fullPath;
|
|
2616
|
+
const values = attributes[entry.node.id];
|
|
2617
|
+
const rawArea = valueOf(values, areaMetric);
|
|
2618
|
+
const index = files.length;
|
|
2619
|
+
fileIndexById.set(entry.node.id, index);
|
|
2620
|
+
files.push({
|
|
2621
|
+
index,
|
|
2622
|
+
nodeId: entry.node.id,
|
|
2623
|
+
path: path9,
|
|
2624
|
+
name: entry.node.name,
|
|
2625
|
+
section: -1,
|
|
2626
|
+
area: Math.max(1, rawArea ?? 0),
|
|
2627
|
+
actualArea: rawArea ?? 0,
|
|
2628
|
+
risk: 0,
|
|
2629
|
+
complexity: valueOf(values, complexityKey) ?? 0,
|
|
2630
|
+
churn: valueOf(values, churnKey) ?? 0,
|
|
2631
|
+
values: []
|
|
2632
|
+
});
|
|
2633
|
+
});
|
|
2634
|
+
if (areaMetric && files.length > 0) {
|
|
2635
|
+
const missing = files.filter((file) => file.actualArea === 0).length;
|
|
2636
|
+
if (missing > 0) {
|
|
2637
|
+
warnings.push(
|
|
2638
|
+
`${missing} ficheros sin "${areaMetric}"; se dibujan con \xE1rea m\xEDnima para no desaparecer`
|
|
2499
2639
|
);
|
|
2500
|
-
const min = optionalNumber(rule.min, `config.rules[${index}].min`);
|
|
2501
|
-
const max = optionalNumber(rule.max, `config.rules[${index}].max`);
|
|
2502
|
-
if (min === void 0 && max === void 0) {
|
|
2503
|
-
throw new ConfigError(
|
|
2504
|
-
`config.rules[${index}] necesita min o max`
|
|
2505
|
-
);
|
|
2506
|
-
}
|
|
2507
|
-
if (min !== void 0 && max !== void 0 && min > max) {
|
|
2508
|
-
throw new ConfigError(
|
|
2509
|
-
`config.rules[${index}]: min no puede ser mayor que max`
|
|
2510
|
-
);
|
|
2511
|
-
}
|
|
2512
|
-
rules.push({ type, name, metric, min, max, severity, message });
|
|
2513
|
-
return;
|
|
2514
2640
|
}
|
|
2515
|
-
if (
|
|
2516
|
-
|
|
2517
|
-
return;
|
|
2641
|
+
if (!options.areaMetric && areaMetric !== "rloc") {
|
|
2642
|
+
warnings.push(`"rloc" no est\xE1 presente; se usa "${areaMetric}" como \xE1rea`);
|
|
2518
2643
|
}
|
|
2519
|
-
throw new ConfigError(
|
|
2520
|
-
`config.rules[${index}].type desconocido: ${type}`
|
|
2521
|
-
);
|
|
2522
|
-
});
|
|
2523
|
-
return rules;
|
|
2524
|
-
}
|
|
2525
|
-
function parseSeverity(value, path8) {
|
|
2526
|
-
if (value === void 0) return "error";
|
|
2527
|
-
if (typeof value !== "string" || !SEVERITIES.has(value)) {
|
|
2528
|
-
throw new ConfigError(`${path8} debe ser info, warn o error`);
|
|
2529
|
-
}
|
|
2530
|
-
return value;
|
|
2531
|
-
}
|
|
2532
|
-
function optionalNumber(value, path8) {
|
|
2533
|
-
if (value === void 0) return void 0;
|
|
2534
|
-
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
2535
|
-
throw new ConfigError(`${path8} debe ser un n\xFAmero`);
|
|
2536
|
-
}
|
|
2537
|
-
return value;
|
|
2538
|
-
}
|
|
2539
|
-
function asObject(value, path8) {
|
|
2540
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2541
|
-
throw new ConfigError(`${path8} debe ser un objeto`);
|
|
2542
2644
|
}
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
return value.trim();
|
|
2550
|
-
}
|
|
2551
|
-
|
|
2552
|
-
// packages/metrics/src/rules.ts
|
|
2553
|
-
import { minimatch } from "minimatch";
|
|
2554
|
-
function emptyRuleReport() {
|
|
2555
|
-
return {
|
|
2556
|
-
configured: false,
|
|
2557
|
-
summary: {
|
|
2558
|
-
layers: 0,
|
|
2559
|
-
rules: 0,
|
|
2560
|
-
introduced: 0,
|
|
2561
|
-
existing: 0,
|
|
2562
|
-
resolved: 0,
|
|
2563
|
-
introducedErrors: 0,
|
|
2564
|
-
introducedWarnings: 0
|
|
2565
|
-
},
|
|
2566
|
-
violations: { introduced: [], existing: [], resolved: [] }
|
|
2567
|
-
};
|
|
2568
|
-
}
|
|
2569
|
-
function evaluateRules(base, head, config) {
|
|
2570
|
-
const baseViolations = evaluateInput(base, config);
|
|
2571
|
-
const headViolations = evaluateInput(head, config);
|
|
2572
|
-
const baseByKey = new Map(
|
|
2573
|
-
baseViolations.map((violation) => [violation.key, violation])
|
|
2574
|
-
);
|
|
2575
|
-
const headByKey = new Map(
|
|
2576
|
-
headViolations.map((violation) => [violation.key, violation])
|
|
2645
|
+
const risks = riskScores(
|
|
2646
|
+
files.map((file) => ({
|
|
2647
|
+
complexity: file.complexity,
|
|
2648
|
+
churn: file.churn,
|
|
2649
|
+
size: file.actualArea
|
|
2650
|
+
}))
|
|
2577
2651
|
);
|
|
2578
|
-
const
|
|
2579
|
-
|
|
2580
|
-
const resolved = [];
|
|
2581
|
-
for (const [key, violation] of headByKey) {
|
|
2582
|
-
if (baseByKey.has(key)) existing.push(violation);
|
|
2583
|
-
else introduced.push(violation);
|
|
2652
|
+
for (const [index, file] of files.entries()) {
|
|
2653
|
+
file.risk = risks[index] ?? 0;
|
|
2584
2654
|
}
|
|
2585
|
-
|
|
2586
|
-
|
|
2655
|
+
const sectionAccumulator = /* @__PURE__ */ new Map();
|
|
2656
|
+
for (const file of files) {
|
|
2657
|
+
const key = sectionForPath(file.path);
|
|
2658
|
+
const current = sectionAccumulator.get(key) ?? { files: 0, area: 0 };
|
|
2659
|
+
current.files++;
|
|
2660
|
+
current.area += file.actualArea;
|
|
2661
|
+
sectionAccumulator.set(key, current);
|
|
2587
2662
|
}
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
resolved: resolved.length,
|
|
2599
|
-
introducedErrors: introduced.filter(
|
|
2600
|
-
(violation) => violation.severity === "error"
|
|
2601
|
-
).length,
|
|
2602
|
-
introducedWarnings: introduced.filter(
|
|
2603
|
-
(violation) => violation.severity === "warn"
|
|
2604
|
-
).length
|
|
2605
|
-
},
|
|
2606
|
-
violations: { introduced, existing, resolved }
|
|
2607
|
-
};
|
|
2608
|
-
}
|
|
2609
|
-
function evaluateInput(input, config) {
|
|
2610
|
-
const layers = layerIndex(input.files, config.layers);
|
|
2611
|
-
const violations = [];
|
|
2612
|
-
for (const rule of config.rules) {
|
|
2613
|
-
if (rule.type === "forbidden") {
|
|
2614
|
-
evaluateForbidden(rule, input, layers, violations);
|
|
2615
|
-
} else if (rule.type === "threshold") {
|
|
2616
|
-
evaluateThreshold(rule, input, violations);
|
|
2617
|
-
} else {
|
|
2618
|
-
evaluateCycles(rule, input.cycles, violations);
|
|
2619
|
-
}
|
|
2663
|
+
const sections = [...sectionAccumulator.entries()].map(([key, value]) => ({
|
|
2664
|
+
key,
|
|
2665
|
+
label: key,
|
|
2666
|
+
files: value.files,
|
|
2667
|
+
area: value.area,
|
|
2668
|
+
color: PALETTE[hashString(key) % PALETTE.length]
|
|
2669
|
+
})).sort((a, b) => b.area - a.area || a.key.localeCompare(b.key));
|
|
2670
|
+
const sectionIndex = new Map(sections.map((section, index) => [section.key, index]));
|
|
2671
|
+
for (const file of files) {
|
|
2672
|
+
file.section = sectionIndex.get(sectionForPath(file.path)) ?? 0;
|
|
2620
2673
|
}
|
|
2621
|
-
const
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2674
|
+
const metrics = metricKeys.map((key) => {
|
|
2675
|
+
const present = files.map((file) => valueOf(attributes[file.nodeId], key)).filter((value) => value !== void 0);
|
|
2676
|
+
const min = present.length > 0 ? Math.min(...present) : 0;
|
|
2677
|
+
const actualMax = present.length > 0 ? Math.max(...present) : 0;
|
|
2678
|
+
const p95 = percentile2(present, 0.95);
|
|
2679
|
+
const skewed = min >= 0 && p95 > 0 && actualMax > p95 * 4;
|
|
2680
|
+
const max = skewed ? actualMax : p95 > 0 ? p95 : actualMax;
|
|
2681
|
+
return {
|
|
2682
|
+
key,
|
|
2683
|
+
title: metricTitle(descriptors, key),
|
|
2684
|
+
direction: metricDirection(descriptors, key),
|
|
2685
|
+
min,
|
|
2686
|
+
max,
|
|
2687
|
+
actualMax,
|
|
2688
|
+
p95,
|
|
2689
|
+
mode: skewed ? "log" : "linear",
|
|
2690
|
+
coverage: present.length
|
|
2691
|
+
};
|
|
2692
|
+
});
|
|
2627
2693
|
for (const file of files) {
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
(pattern) => minimatch(file.path, pattern, { dot: true })
|
|
2631
|
-
)) {
|
|
2632
|
-
index.set(file.id, layer.name);
|
|
2633
|
-
break;
|
|
2634
|
-
}
|
|
2635
|
-
}
|
|
2694
|
+
const values = attributes[file.nodeId];
|
|
2695
|
+
file.values = metrics.map((metric) => valueOf(values, metric.key) ?? null);
|
|
2636
2696
|
}
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2697
|
+
const roots = buildRoots(cc, singleRoot, fileIndexById, files);
|
|
2698
|
+
const folderCount = countFolders(roots);
|
|
2699
|
+
const dependency = cc.lenses?.dependency;
|
|
2700
|
+
const leafEdges = asEdges(dependency?.leafEdges);
|
|
2701
|
+
const rawEdges = leafEdges.length > 0 ? leafEdges : asEdges(dependency?.edges);
|
|
2702
|
+
const edgeSet = /* @__PURE__ */ new Set();
|
|
2703
|
+
const edges = [];
|
|
2704
|
+
const n = files.length;
|
|
2705
|
+
for (const edge of rawEdges) {
|
|
2706
|
+
const from = fileIndexById.get(edge.fromId);
|
|
2707
|
+
const to = fileIndexById.get(edge.toId);
|
|
2708
|
+
if (from === void 0 || to === void 0 || from === to) continue;
|
|
2709
|
+
const key = from * n + to;
|
|
2710
|
+
if (edgeSet.has(key)) continue;
|
|
2711
|
+
edgeSet.add(key);
|
|
2712
|
+
edges.push({ from, to });
|
|
2713
|
+
if (edges.length >= 2e4) {
|
|
2714
|
+
warnings.push("se han limitado las dependencias a 20.000 aristas");
|
|
2715
|
+
break;
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
edges.sort((a, b) => a.from - b.from || a.to - b.to);
|
|
2719
|
+
const flaggedEdges = /* @__PURE__ */ new Set();
|
|
2720
|
+
for (const edge of rawEdges) {
|
|
2721
|
+
if (edge.isCyclic !== true) continue;
|
|
2722
|
+
const from = fileIndexById.get(edge.fromId);
|
|
2723
|
+
const to = fileIndexById.get(edge.toId);
|
|
2724
|
+
if (from === void 0 || to === void 0) continue;
|
|
2725
|
+
flaggedEdges.add(from * n + to);
|
|
2726
|
+
}
|
|
2727
|
+
const cycleSet = /* @__PURE__ */ new Set();
|
|
2728
|
+
const cycles = [];
|
|
2729
|
+
for (const edge of edges) {
|
|
2730
|
+
const key = edge.from * n + edge.to;
|
|
2731
|
+
const backward = edgeSet.has(edge.to * n + edge.from);
|
|
2732
|
+
if (backward || flaggedEdges.has(key)) {
|
|
2733
|
+
if (!cycleSet.has(key)) {
|
|
2734
|
+
cycleSet.add(key);
|
|
2735
|
+
cycles.push(edge);
|
|
2655
2736
|
}
|
|
2656
|
-
}
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
cycles.sort((a, b) => a.from - b.from || a.to - b.to);
|
|
2740
|
+
if (rawEdges.length === 0) {
|
|
2741
|
+
warnings.push("este cc.json no trae una lente de dependencias utilizable");
|
|
2742
|
+
}
|
|
2743
|
+
const totalArea = files.reduce((sum, file) => sum + file.actualArea, 0);
|
|
2744
|
+
const hotspotCount = files.filter((file) => file.risk >= 0.66).length;
|
|
2745
|
+
let initialLens = "risk";
|
|
2746
|
+
let initialMetric = null;
|
|
2747
|
+
if (options.color === "structure") {
|
|
2748
|
+
initialLens = "structure";
|
|
2749
|
+
} else if (options.color && options.color !== "risk") {
|
|
2750
|
+
if (!coverage.has(options.color)) {
|
|
2751
|
+
throw new RenderError(`la m\xE9trica de color "${options.color}" no existe en este cc.json`);
|
|
2752
|
+
}
|
|
2753
|
+
initialLens = "metric";
|
|
2754
|
+
initialMetric = options.color;
|
|
2657
2755
|
}
|
|
2756
|
+
const defaultSelection = pickDefaultSelection(files, initialLens, initialMetric, metrics);
|
|
2757
|
+
const project = cc.meta.projectName || options.title || "Proyecto sin nombre";
|
|
2758
|
+
return {
|
|
2759
|
+
meta: {
|
|
2760
|
+
project,
|
|
2761
|
+
title: options.title || project,
|
|
2762
|
+
commit: cc.meta.commitHash || "",
|
|
2763
|
+
checksum: cc.meta.checksum || "",
|
|
2764
|
+
apiVersion: cc.meta.apiVersion,
|
|
2765
|
+
rootName
|
|
2766
|
+
},
|
|
2767
|
+
files,
|
|
2768
|
+
sections,
|
|
2769
|
+
metrics,
|
|
2770
|
+
edges,
|
|
2771
|
+
cycles,
|
|
2772
|
+
roots,
|
|
2773
|
+
areaMetric,
|
|
2774
|
+
areaTitle: areaMetric ? metricTitle(descriptors, areaMetric) : "\xC1rea",
|
|
2775
|
+
totalArea,
|
|
2776
|
+
fileCount: files.length,
|
|
2777
|
+
folderCount,
|
|
2778
|
+
hotspotCount,
|
|
2779
|
+
defaultSelection,
|
|
2780
|
+
initialLens,
|
|
2781
|
+
initialMetric,
|
|
2782
|
+
warnings
|
|
2783
|
+
};
|
|
2658
2784
|
}
|
|
2659
|
-
function
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2785
|
+
function buildRoots(cc, singleRoot, fileIndexById, files) {
|
|
2786
|
+
const build = (node, path9) => {
|
|
2787
|
+
if (node.type === "File") {
|
|
2788
|
+
const index = fileIndexById.get(node.id);
|
|
2789
|
+
const file = index === void 0 ? void 0 : files[index];
|
|
2790
|
+
return {
|
|
2791
|
+
kind: "file",
|
|
2792
|
+
key: `f:${index ?? -1}`,
|
|
2793
|
+
name: node.name,
|
|
2794
|
+
path: path9,
|
|
2795
|
+
value: file?.area ?? 1,
|
|
2796
|
+
fileCount: 1,
|
|
2797
|
+
section: file?.section ?? 0,
|
|
2798
|
+
fileIndex: index
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
const children = (node.children ?? []).map(
|
|
2802
|
+
(child) => build(child, path9 ? `${path9}/${child.name}` : child.name)
|
|
2803
|
+
);
|
|
2804
|
+
const folder = {
|
|
2805
|
+
kind: "folder",
|
|
2806
|
+
key: `d:${path9}`,
|
|
2807
|
+
name: node.name,
|
|
2808
|
+
path: path9,
|
|
2809
|
+
value: children.reduce((sum, child) => sum + child.value, 0) || 1,
|
|
2810
|
+
fileCount: children.reduce((sum, child) => sum + child.fileCount, 0),
|
|
2811
|
+
section: -1,
|
|
2812
|
+
children
|
|
2813
|
+
};
|
|
2814
|
+
while (folder.children?.length === 1 && folder.children[0]?.kind === "folder") {
|
|
2815
|
+
const only = folder.children[0];
|
|
2816
|
+
folder.name = `${folder.name}/${only.name}`;
|
|
2817
|
+
folder.path = only.path;
|
|
2818
|
+
folder.key = `d:${only.path}`;
|
|
2819
|
+
folder.value = only.value;
|
|
2820
|
+
folder.fileCount = only.fileCount;
|
|
2821
|
+
folder.children = only.children;
|
|
2822
|
+
}
|
|
2823
|
+
return folder;
|
|
2824
|
+
};
|
|
2825
|
+
if (singleRoot) {
|
|
2826
|
+
return (singleRoot.children ?? []).map((child) => build(child, child.name));
|
|
2827
|
+
}
|
|
2828
|
+
return cc.files.map((root) => build(root, root.name));
|
|
2829
|
+
}
|
|
2830
|
+
function countFolders(nodes) {
|
|
2831
|
+
let count = 0;
|
|
2832
|
+
const stack = [...nodes];
|
|
2833
|
+
while (stack.length > 0) {
|
|
2834
|
+
const node = stack.pop();
|
|
2835
|
+
if (node.kind === "folder") count++;
|
|
2836
|
+
if (node.children) stack.push(...node.children);
|
|
2681
2837
|
}
|
|
2838
|
+
return count;
|
|
2682
2839
|
}
|
|
2683
|
-
function
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
cycle: {
|
|
2694
|
-
size: cycle.size,
|
|
2695
|
-
nodes: cycle.nodes.map((node) => ({ ...node }))
|
|
2840
|
+
function pickDefaultSelection(files, lens, metric, metrics) {
|
|
2841
|
+
if (files.length === 0) return null;
|
|
2842
|
+
if (lens === "metric" && metric) {
|
|
2843
|
+
const index = metrics.findIndex((entry) => entry.key === metric);
|
|
2844
|
+
if (index >= 0) {
|
|
2845
|
+
let best = null;
|
|
2846
|
+
for (const file of files) {
|
|
2847
|
+
const value = file.values[index];
|
|
2848
|
+
if (value === null || value === void 0) continue;
|
|
2849
|
+
if (!best || value > (best.values[index] ?? -Infinity)) best = file;
|
|
2696
2850
|
}
|
|
2697
|
-
|
|
2851
|
+
if (best) return best.index;
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
if (lens === "structure") {
|
|
2855
|
+
return files.reduce(
|
|
2856
|
+
(best, file) => file.actualArea > best.actualArea ? file : best
|
|
2857
|
+
).index;
|
|
2698
2858
|
}
|
|
2859
|
+
return files.reduce((best, file) => file.risk > best.risk ? file : best).index;
|
|
2699
2860
|
}
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2861
|
+
|
|
2862
|
+
// packages/render/src/html.ts
|
|
2863
|
+
import { readFileSync } from "node:fs";
|
|
2864
|
+
|
|
2865
|
+
// packages/render/src/color.ts
|
|
2866
|
+
var RISK_STOPS = [
|
|
2867
|
+
[0, "#ebe7df"],
|
|
2868
|
+
[0.35, "#e9cd98"],
|
|
2869
|
+
[0.65, "#d68a54"],
|
|
2870
|
+
[1, "#a42e2c"]
|
|
2871
|
+
];
|
|
2872
|
+
var METRIC_STOPS = [
|
|
2873
|
+
[0, "#ebe7df"],
|
|
2874
|
+
[0.5, "#b9c9cf"],
|
|
2875
|
+
[1, "#355b6b"]
|
|
2876
|
+
];
|
|
2877
|
+
var RISK_GRADIENT = "linear-gradient(90deg, #ebe7df 0%, #e9cd98 35%, #d68a54 65%, #a42e2c 100%)";
|
|
2878
|
+
var METRIC_GRADIENT = "linear-gradient(90deg, #ebe7df 0%, #b9c9cf 50%, #355b6b 100%)";
|
|
2879
|
+
function clamp012(value) {
|
|
2880
|
+
if (!Number.isFinite(value)) return 0;
|
|
2881
|
+
return Math.max(0, Math.min(1, value));
|
|
2704
2882
|
}
|
|
2705
|
-
function
|
|
2706
|
-
|
|
2883
|
+
function hexToRgb(hex) {
|
|
2884
|
+
const value = hex.replace("#", "");
|
|
2885
|
+
return [
|
|
2886
|
+
Number.parseInt(value.slice(0, 2), 16),
|
|
2887
|
+
Number.parseInt(value.slice(2, 4), 16),
|
|
2888
|
+
Number.parseInt(value.slice(4, 6), 16)
|
|
2889
|
+
];
|
|
2707
2890
|
}
|
|
2708
|
-
function
|
|
2709
|
-
return
|
|
2891
|
+
function rgbToHex(rgb) {
|
|
2892
|
+
return `#${rgb.map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
|
|
2710
2893
|
}
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
const changed = changedFiles(baseIndex, headIndex);
|
|
2720
|
-
const edgeDiff = diffEdges(baseIndex, headIndex);
|
|
2721
|
-
const baseCycles = cycleGroups(baseIndex);
|
|
2722
|
-
const headCycles = cycleGroups(headIndex);
|
|
2723
|
-
const introducedCycles = cycleDifference(headCycles, baseCycles);
|
|
2724
|
-
const fixedCycles = cycleDifference(baseCycles, headCycles);
|
|
2725
|
-
const hotspotMetric = chooseHotspotMetric(
|
|
2726
|
-
baseIndex,
|
|
2727
|
-
headIndex,
|
|
2728
|
-
options.hotspotMetric
|
|
2729
|
-
);
|
|
2730
|
-
const hotspots = hotspotMetric ? buildHotspots(baseIndex, headIndex, added, changed, hotspotMetric, top) : [];
|
|
2731
|
-
const rules = options.rules ? evaluateRules(
|
|
2732
|
-
ruleInput(baseIndex, [...baseCycles.values()].map((group) => group.component)),
|
|
2733
|
-
ruleInput(headIndex, [...headCycles.values()].map((group) => group.component)),
|
|
2734
|
-
options.rules
|
|
2735
|
-
) : emptyRuleReport();
|
|
2736
|
-
return {
|
|
2737
|
-
schemaVersion: "1.0",
|
|
2738
|
-
base: summarize(baseIndex),
|
|
2739
|
-
head: summarize(headIndex),
|
|
2740
|
-
summary: {
|
|
2741
|
-
addedFiles: added.length,
|
|
2742
|
-
removedFiles: removed.length,
|
|
2743
|
-
changedFiles: changed.length,
|
|
2744
|
-
addedEdges: edgeDiff.added.length,
|
|
2745
|
-
removedEdges: edgeDiff.removed.length,
|
|
2746
|
-
cyclicEdgesAdded: edgeDiff.cyclicAdded.length,
|
|
2747
|
-
cyclicEdgesRemoved: edgeDiff.cyclicRemoved.length,
|
|
2748
|
-
introducedCycles: introducedCycles.length,
|
|
2749
|
-
fixedCycles: fixedCycles.length,
|
|
2750
|
-
hotspots: hotspots.length
|
|
2751
|
-
},
|
|
2752
|
-
hotspotMetric,
|
|
2753
|
-
rules,
|
|
2754
|
-
files: { added, removed, changed },
|
|
2755
|
-
edges: edgeDiff,
|
|
2756
|
-
cycles: { introduced: introducedCycles, fixed: fixedCycles },
|
|
2757
|
-
hotspots
|
|
2758
|
-
};
|
|
2894
|
+
function mix(a, b, t) {
|
|
2895
|
+
const from = hexToRgb(a);
|
|
2896
|
+
const to = hexToRgb(b);
|
|
2897
|
+
return rgbToHex([
|
|
2898
|
+
Math.round(from[0] + (to[0] - from[0]) * t),
|
|
2899
|
+
Math.round(from[1] + (to[1] - from[1]) * t),
|
|
2900
|
+
Math.round(from[2] + (to[2] - from[2]) * t)
|
|
2901
|
+
]);
|
|
2759
2902
|
}
|
|
2760
|
-
function
|
|
2761
|
-
const
|
|
2762
|
-
for (
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
(left, right) => compareStrings5(paths.get(left) ?? left, paths.get(right) ?? right)
|
|
2769
|
-
);
|
|
2770
|
-
const metrics = cc.lenses?.metrics?.attributes ?? {};
|
|
2771
|
-
const edges = /* @__PURE__ */ new Map();
|
|
2772
|
-
for (const edge of cc.lenses?.dependency?.edges ?? []) {
|
|
2773
|
-
const key = edgeKey(edge.fromId, edge.toId);
|
|
2774
|
-
const weight = finiteNumber(edge.attributes?.weight) ?? 1;
|
|
2775
|
-
const typeOnly = finiteNumber(edge.attributes?.typeOnly) ?? 0;
|
|
2776
|
-
const existing = edges.get(key);
|
|
2777
|
-
if (existing) {
|
|
2778
|
-
existing.weight += weight;
|
|
2779
|
-
existing.typeOnly += typeOnly;
|
|
2780
|
-
existing.isCyclic = existing.isCyclic || edge.isCyclic === true;
|
|
2781
|
-
} else {
|
|
2782
|
-
edges.set(key, {
|
|
2783
|
-
fromId: edge.fromId,
|
|
2784
|
-
toId: edge.toId,
|
|
2785
|
-
weight,
|
|
2786
|
-
typeOnly,
|
|
2787
|
-
isCyclic: edge.isCyclic === true
|
|
2788
|
-
});
|
|
2903
|
+
function ramp(stops, t) {
|
|
2904
|
+
const value = clamp012(t);
|
|
2905
|
+
for (let index = 0; index < stops.length - 1; index++) {
|
|
2906
|
+
const from = stops[index];
|
|
2907
|
+
const to = stops[index + 1];
|
|
2908
|
+
if (value <= to[0]) {
|
|
2909
|
+
const span2 = to[0] - from[0] || 1;
|
|
2910
|
+
return mix(from[1], to[1], (value - from[0]) / span2);
|
|
2789
2911
|
}
|
|
2790
2912
|
}
|
|
2791
|
-
return
|
|
2792
|
-
}
|
|
2793
|
-
function relativePath(root, fullPath, leaf) {
|
|
2794
|
-
if (fullPath === root.name) return leaf.name;
|
|
2795
|
-
const prefix = `${root.name}/`;
|
|
2796
|
-
return fullPath.startsWith(prefix) ? fullPath.slice(prefix.length) : fullPath;
|
|
2913
|
+
return stops[stops.length - 1][1];
|
|
2797
2914
|
}
|
|
2798
|
-
function
|
|
2799
|
-
|
|
2800
|
-
walkTree(index.cc.files, (entry) => {
|
|
2801
|
-
if (entry.node.type === "Folder" && entry.depth > 0) folders++;
|
|
2802
|
-
});
|
|
2803
|
-
return {
|
|
2804
|
-
projectName: index.cc.meta.projectName ?? "",
|
|
2805
|
-
checksum: index.cc.meta.checksum ?? "",
|
|
2806
|
-
files: index.paths.size,
|
|
2807
|
-
folders,
|
|
2808
|
-
edges: index.edges.size
|
|
2809
|
-
};
|
|
2915
|
+
function riskColor(risk) {
|
|
2916
|
+
return ramp(RISK_STOPS, risk);
|
|
2810
2917
|
}
|
|
2811
|
-
function
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2918
|
+
function metricColor(value, metric) {
|
|
2919
|
+
if (value === null) return METRIC_STOPS[0][1];
|
|
2920
|
+
let t;
|
|
2921
|
+
if (metric.mode === "log") {
|
|
2922
|
+
const span2 = Math.max(1e-9, metric.max - metric.min);
|
|
2923
|
+
t = Math.log1p(Math.max(0, value - metric.min)) / Math.log1p(span2);
|
|
2924
|
+
} else {
|
|
2925
|
+
const span2 = metric.max - metric.min;
|
|
2926
|
+
t = span2 > 0 ? (value - metric.min) / span2 : 0;
|
|
2820
2927
|
}
|
|
2821
|
-
return
|
|
2928
|
+
return ramp(METRIC_STOPS, t);
|
|
2822
2929
|
}
|
|
2823
|
-
function
|
|
2824
|
-
const
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2930
|
+
function inkFor(color) {
|
|
2931
|
+
const [r, g, b] = hexToRgb(color);
|
|
2932
|
+
const luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
|
|
2933
|
+
return luminance > 0.52 ? "#1a1a1a" : "#f7f5f1";
|
|
2934
|
+
}
|
|
2935
|
+
function colorForFile(lens, risk, section, value, metric) {
|
|
2936
|
+
if (lens === "structure") return section?.color ?? "#ddd8cf";
|
|
2937
|
+
if (lens === "risk" || !metric) return riskColor(risk);
|
|
2938
|
+
return metricColor(value, metric);
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
// packages/layouts/src/squarify.ts
|
|
2942
|
+
function squarify(items, rect) {
|
|
2943
|
+
if (rect.w <= 0 || rect.h <= 0) return [];
|
|
2944
|
+
const usable = items.filter(
|
|
2945
|
+
(item) => Number.isFinite(item.value) && item.value > 0
|
|
2946
|
+
);
|
|
2947
|
+
const total = usable.reduce((sum, item) => sum + item.value, 0);
|
|
2948
|
+
if (usable.length === 0 || total <= 0) return [];
|
|
2949
|
+
const sorted = usable.map((item, index2) => ({ item, index: index2 })).sort((a, b) => b.item.value - a.item.value || a.index - b.index).map(({ item }) => item);
|
|
2950
|
+
const scale = rect.w * rect.h / total;
|
|
2951
|
+
const placed = [];
|
|
2952
|
+
let x = rect.x;
|
|
2953
|
+
let y = rect.y;
|
|
2954
|
+
let w = rect.w;
|
|
2955
|
+
let h = rect.h;
|
|
2956
|
+
let row = [];
|
|
2957
|
+
const area = (item) => item.value * scale;
|
|
2958
|
+
function worst(candidate, side) {
|
|
2959
|
+
let sum = 0;
|
|
2960
|
+
let max = -Infinity;
|
|
2961
|
+
let min = Infinity;
|
|
2962
|
+
for (const item of candidate) {
|
|
2963
|
+
const value = area(item);
|
|
2964
|
+
sum += value;
|
|
2965
|
+
if (value > max) max = value;
|
|
2966
|
+
if (value < min) min = value;
|
|
2844
2967
|
}
|
|
2845
|
-
if (
|
|
2968
|
+
if (sum <= 0 || side <= 0) return Infinity;
|
|
2969
|
+
return Math.max(
|
|
2970
|
+
side * side * max / (sum * sum),
|
|
2971
|
+
sum * sum / (side * side * min)
|
|
2972
|
+
);
|
|
2846
2973
|
}
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2974
|
+
function place(current, horizontal) {
|
|
2975
|
+
if (current.length === 0) return;
|
|
2976
|
+
const sum = current.reduce((acc, item) => acc + area(item), 0);
|
|
2977
|
+
if (sum <= 0) return;
|
|
2978
|
+
if (horizontal) {
|
|
2979
|
+
if (w <= 0) return;
|
|
2980
|
+
const thickness = sum / w;
|
|
2981
|
+
if (!Number.isFinite(thickness) || thickness <= 0) return;
|
|
2982
|
+
let position = x;
|
|
2983
|
+
for (const item of current) {
|
|
2984
|
+
const length = area(item) / thickness;
|
|
2985
|
+
placed.push({ item, x: position, y, w: length, h: thickness });
|
|
2986
|
+
position += length;
|
|
2987
|
+
}
|
|
2988
|
+
y += thickness;
|
|
2989
|
+
h -= thickness;
|
|
2990
|
+
} else {
|
|
2991
|
+
if (h <= 0) return;
|
|
2992
|
+
const thickness = sum / h;
|
|
2993
|
+
if (!Number.isFinite(thickness) || thickness <= 0) return;
|
|
2994
|
+
let position = y;
|
|
2995
|
+
for (const item of current) {
|
|
2996
|
+
const length = area(item) / thickness;
|
|
2997
|
+
placed.push({ item, x, y: position, w: thickness, h: length });
|
|
2998
|
+
position += length;
|
|
2999
|
+
}
|
|
3000
|
+
x += thickness;
|
|
3001
|
+
w -= thickness;
|
|
3002
|
+
}
|
|
2854
3003
|
}
|
|
2855
|
-
|
|
2856
|
-
|
|
3004
|
+
let index = 0;
|
|
3005
|
+
while (index < sorted.length) {
|
|
3006
|
+
const horizontal = w >= h;
|
|
3007
|
+
const side = horizontal ? w : h;
|
|
3008
|
+
const next = sorted[index];
|
|
3009
|
+
if (row.length === 0) {
|
|
3010
|
+
row.push(next);
|
|
3011
|
+
index++;
|
|
3012
|
+
continue;
|
|
3013
|
+
}
|
|
3014
|
+
if (worst([...row, next], side) <= worst(row, side)) {
|
|
3015
|
+
row.push(next);
|
|
3016
|
+
index++;
|
|
3017
|
+
} else {
|
|
3018
|
+
place(row, horizontal);
|
|
3019
|
+
row = [];
|
|
3020
|
+
}
|
|
2857
3021
|
}
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
3022
|
+
if (row.length > 0) place(row, w >= h);
|
|
3023
|
+
return placed;
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
// packages/render/src/layout.ts
|
|
3027
|
+
var VIEW_WIDTH = 1400;
|
|
3028
|
+
var VIEW_HEIGHT = 900;
|
|
3029
|
+
var VIEW_PADDING = 8;
|
|
3030
|
+
var MAX_DEPTH3 = 12;
|
|
3031
|
+
function layoutMap(roots, width = VIEW_WIDTH, height = VIEW_HEIGHT) {
|
|
3032
|
+
const rect = {
|
|
3033
|
+
x: VIEW_PADDING,
|
|
3034
|
+
y: VIEW_PADDING,
|
|
3035
|
+
w: Math.max(1, width - VIEW_PADDING * 2),
|
|
3036
|
+
h: Math.max(1, height - VIEW_PADDING * 2)
|
|
2865
3037
|
};
|
|
3038
|
+
return layoutChildren(roots, rect, 0);
|
|
2866
3039
|
}
|
|
2867
|
-
function
|
|
2868
|
-
return
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
3040
|
+
function layoutChildren(nodes, rect, depth) {
|
|
3041
|
+
if (nodes.length === 0 || rect.w < 2 || rect.h < 2) return [];
|
|
3042
|
+
const items = nodes.map((node) => ({
|
|
3043
|
+
key: node.key,
|
|
3044
|
+
value: Math.max(1, node.value),
|
|
3045
|
+
node
|
|
3046
|
+
}));
|
|
3047
|
+
return squarify(items, rect).map(
|
|
3048
|
+
(placed) => layoutNode(placed.item.node, {
|
|
3049
|
+
x: placed.x,
|
|
3050
|
+
y: placed.y,
|
|
3051
|
+
w: placed.w,
|
|
3052
|
+
h: placed.h
|
|
3053
|
+
}, depth)
|
|
3054
|
+
);
|
|
2877
3055
|
}
|
|
2878
|
-
function
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
for (const node of index.nodes) {
|
|
2882
|
-
adjacency.set(node, []);
|
|
2883
|
-
reverse.set(node, []);
|
|
2884
|
-
}
|
|
2885
|
-
for (const edge of index.edges.values()) {
|
|
2886
|
-
if (!index.paths.has(edge.fromId) || !index.paths.has(edge.toId)) continue;
|
|
2887
|
-
adjacency.get(edge.fromId)?.push(edge.toId);
|
|
2888
|
-
reverse.get(edge.toId)?.push(edge.fromId);
|
|
2889
|
-
}
|
|
2890
|
-
for (const [node, neighbors] of adjacency) {
|
|
2891
|
-
adjacency.set(node, [...new Set(neighbors)].sort(compareStrings5));
|
|
2892
|
-
}
|
|
2893
|
-
for (const [node, neighbors] of reverse) {
|
|
2894
|
-
reverse.set(node, [...new Set(neighbors)].sort(compareStrings5));
|
|
2895
|
-
}
|
|
2896
|
-
const visited = /* @__PURE__ */ new Set();
|
|
2897
|
-
const order = [];
|
|
2898
|
-
for (const start of index.nodes) {
|
|
2899
|
-
if (visited.has(start)) continue;
|
|
2900
|
-
visited.add(start);
|
|
2901
|
-
const stack = [
|
|
2902
|
-
{ node: start, neighbors: adjacency.get(start) ?? [], next: 0 }
|
|
2903
|
-
];
|
|
2904
|
-
while (stack.length > 0) {
|
|
2905
|
-
const frame = stack[stack.length - 1];
|
|
2906
|
-
if (frame.next < frame.neighbors.length) {
|
|
2907
|
-
const next = frame.neighbors[frame.next];
|
|
2908
|
-
frame.next++;
|
|
2909
|
-
if (!visited.has(next)) {
|
|
2910
|
-
visited.add(next);
|
|
2911
|
-
stack.push({
|
|
2912
|
-
node: next,
|
|
2913
|
-
neighbors: adjacency.get(next) ?? [],
|
|
2914
|
-
next: 0
|
|
2915
|
-
});
|
|
2916
|
-
}
|
|
2917
|
-
} else {
|
|
2918
|
-
order.push(frame.node);
|
|
2919
|
-
stack.pop();
|
|
2920
|
-
}
|
|
2921
|
-
}
|
|
2922
|
-
}
|
|
2923
|
-
const assigned = /* @__PURE__ */ new Set();
|
|
2924
|
-
const groups = /* @__PURE__ */ new Map();
|
|
2925
|
-
for (let position = order.length - 1; position >= 0; position--) {
|
|
2926
|
-
const start = order[position];
|
|
2927
|
-
if (assigned.has(start)) continue;
|
|
2928
|
-
assigned.add(start);
|
|
2929
|
-
const component = [];
|
|
2930
|
-
const stack = [start];
|
|
2931
|
-
while (stack.length > 0) {
|
|
2932
|
-
const node = stack.pop();
|
|
2933
|
-
component.push(node);
|
|
2934
|
-
for (const next of reverse.get(node) ?? []) {
|
|
2935
|
-
if (assigned.has(next)) continue;
|
|
2936
|
-
assigned.add(next);
|
|
2937
|
-
stack.push(next);
|
|
2938
|
-
}
|
|
2939
|
-
}
|
|
2940
|
-
const hasSelfLoop = component.some(
|
|
2941
|
-
(node) => (adjacency.get(node) ?? []).includes(node)
|
|
2942
|
-
);
|
|
2943
|
-
if (component.length < 2 && !hasSelfLoop) continue;
|
|
2944
|
-
const nodes = component.map((id) => ({
|
|
2945
|
-
id,
|
|
2946
|
-
path: index.paths.get(id) ?? id
|
|
2947
|
-
})).sort((left, right) => {
|
|
2948
|
-
return compareStrings5(left.path, right.path) || compareStrings5(left.id, right.id);
|
|
2949
|
-
});
|
|
2950
|
-
const key = [...component].sort(compareStrings5).join("\0");
|
|
2951
|
-
groups.set(key, { key, component: { size: nodes.length, nodes } });
|
|
3056
|
+
function layoutNode(node, rect, depth) {
|
|
3057
|
+
if (node.kind === "file" || !node.children || node.children.length === 0) {
|
|
3058
|
+
return { node, rect, headerHeight: 0, children: [] };
|
|
2952
3059
|
}
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
const cycles = [];
|
|
2957
|
-
for (const [key, group] of present) {
|
|
2958
|
-
if (!absent.has(key)) cycles.push(group.component);
|
|
3060
|
+
let headerHeight = 0;
|
|
3061
|
+
if (rect.w >= 30 && rect.h >= 32) {
|
|
3062
|
+
headerHeight = rect.h >= 62 ? 17 : 13;
|
|
2959
3063
|
}
|
|
2960
|
-
|
|
3064
|
+
const inner = {
|
|
3065
|
+
x: rect.x + 1,
|
|
3066
|
+
y: rect.y + headerHeight + 1,
|
|
3067
|
+
w: Math.max(0, rect.w - 2),
|
|
3068
|
+
h: Math.max(0, rect.h - headerHeight - 2)
|
|
3069
|
+
};
|
|
3070
|
+
const children = depth < MAX_DEPTH3 && inner.w >= 1 && inner.h >= 1 ? layoutChildren(node.children, inner, depth + 1) : [];
|
|
3071
|
+
return { node, rect, headerHeight, children };
|
|
2961
3072
|
}
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
metrics: sortedMetrics(index.metrics[id])
|
|
2967
|
-
}));
|
|
2968
|
-
const edges = [];
|
|
2969
|
-
for (const edge of index.edges.values()) {
|
|
2970
|
-
if (!index.paths.has(edge.fromId) || !index.paths.has(edge.toId)) {
|
|
2971
|
-
continue;
|
|
2972
|
-
}
|
|
2973
|
-
edges.push({
|
|
2974
|
-
fromId: edge.fromId,
|
|
2975
|
-
toId: edge.toId,
|
|
2976
|
-
from: index.paths.get(edge.fromId) ?? edge.fromId,
|
|
2977
|
-
to: index.paths.get(edge.toId) ?? edge.toId
|
|
2978
|
-
});
|
|
2979
|
-
}
|
|
2980
|
-
return { files, edges, cycles };
|
|
3073
|
+
|
|
3074
|
+
// packages/render/src/svg.ts
|
|
3075
|
+
function escapeHtml(value) {
|
|
3076
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2981
3077
|
}
|
|
2982
|
-
function
|
|
2983
|
-
|
|
2984
|
-
for (const metric of ["maxComplexity", "rloc"]) {
|
|
2985
|
-
if (hasMetric(base, metric) || hasMetric(head, metric)) return metric;
|
|
2986
|
-
}
|
|
2987
|
-
return null;
|
|
3078
|
+
function escapeAttr(value) {
|
|
3079
|
+
return escapeHtml(value);
|
|
2988
3080
|
}
|
|
2989
|
-
function
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
}
|
|
2993
|
-
return false;
|
|
3081
|
+
function formatInt(value) {
|
|
3082
|
+
if (!Number.isFinite(value)) return "\u2014";
|
|
3083
|
+
return Math.round(value).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
|
|
2994
3084
|
}
|
|
2995
|
-
function
|
|
2996
|
-
|
|
2997
|
-
const
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
const baseValue = finiteNumber(base.metrics[file.id]?.[metric]);
|
|
3001
|
-
const headValue = finiteNumber(head.metrics[file.id]?.[metric]);
|
|
3002
|
-
if (headValue === null) continue;
|
|
3003
|
-
const before = baseValue ?? 0;
|
|
3004
|
-
const delta = headValue - before;
|
|
3005
|
-
if (delta <= 0) continue;
|
|
3006
|
-
hotspots.push({
|
|
3007
|
-
id: file.id,
|
|
3008
|
-
path: file.path,
|
|
3009
|
-
metric,
|
|
3010
|
-
base: before,
|
|
3011
|
-
head: headValue,
|
|
3012
|
-
delta,
|
|
3013
|
-
change: addedIds.has(file.id) ? "added" : "changed"
|
|
3014
|
-
});
|
|
3085
|
+
function formatCompact(value) {
|
|
3086
|
+
if (!Number.isFinite(value)) return "\u2014";
|
|
3087
|
+
const absolute = Math.abs(value);
|
|
3088
|
+
if (absolute >= 1e6) {
|
|
3089
|
+
return `${(value / 1e6).toFixed(1).replace(".0", "").replace(".", ",")}M`;
|
|
3015
3090
|
}
|
|
3016
|
-
|
|
3017
|
-
(
|
|
3018
|
-
).slice(0, top);
|
|
3019
|
-
}
|
|
3020
|
-
function sortedMetrics(values) {
|
|
3021
|
-
const sorted = {};
|
|
3022
|
-
if (!values) return sorted;
|
|
3023
|
-
for (const key of Object.keys(values).sort(compareStrings5)) {
|
|
3024
|
-
const value = finiteNumber(values[key]);
|
|
3025
|
-
if (value !== null) sorted[key] = value;
|
|
3091
|
+
if (absolute >= 1e3) {
|
|
3092
|
+
return `${(value / 1e3).toFixed(1).replace(".0", "").replace(".", ",")}k`;
|
|
3026
3093
|
}
|
|
3027
|
-
return
|
|
3028
|
-
}
|
|
3029
|
-
function finiteNumber(value) {
|
|
3030
|
-
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
3031
|
-
}
|
|
3032
|
-
function edgeKey(fromId, toId) {
|
|
3033
|
-
return `${fromId}\0${toId}`;
|
|
3034
|
-
}
|
|
3035
|
-
function compareFiles(left, right) {
|
|
3036
|
-
return compareStrings5(left.path, right.path) || compareStrings5(left.id, right.id);
|
|
3094
|
+
return formatInt(value);
|
|
3037
3095
|
}
|
|
3038
|
-
function
|
|
3039
|
-
|
|
3096
|
+
function coord(value) {
|
|
3097
|
+
const rounded = Math.round(value);
|
|
3098
|
+
return Object.is(rounded, -0) ? "0" : String(rounded);
|
|
3040
3099
|
}
|
|
3041
|
-
function
|
|
3042
|
-
return
|
|
3100
|
+
function span(value) {
|
|
3101
|
+
return String(Math.max(1, Math.round(value)));
|
|
3043
3102
|
}
|
|
3044
|
-
function
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3103
|
+
function truncate(value, maxWidth, fontSize) {
|
|
3104
|
+
const maxChars = Math.floor(maxWidth / (fontSize * 0.56));
|
|
3105
|
+
if (maxChars <= 0) return "";
|
|
3106
|
+
if (value.length <= maxChars) return value;
|
|
3107
|
+
if (maxChars === 1) return "\u2026";
|
|
3108
|
+
return `${value.slice(0, maxChars - 1)}\u2026`;
|
|
3049
3109
|
}
|
|
3050
|
-
function
|
|
3051
|
-
|
|
3110
|
+
function labelFor(nodeName, rect, out, ink) {
|
|
3111
|
+
if (rect.w < 44 || rect.h < 13) return;
|
|
3112
|
+
const available = rect.w - 8;
|
|
3113
|
+
const dot = nodeName.lastIndexOf(".");
|
|
3114
|
+
const twoLines = rect.h >= 27 && rect.w >= 64 && dot > 0 && dot < nodeName.length - 1;
|
|
3115
|
+
if (twoLines) {
|
|
3116
|
+
const base = truncate(nodeName.slice(0, dot), available, 12.5);
|
|
3117
|
+
const extension = truncate(nodeName.slice(dot), available, 10);
|
|
3118
|
+
if (base) {
|
|
3119
|
+
out.push(
|
|
3120
|
+
`<text class="a" x="${coord(rect.x + 4)}" y="${coord(rect.y + 12)}" fill="${ink}">${escapeHtml(base)}</text>`
|
|
3121
|
+
);
|
|
3122
|
+
}
|
|
3123
|
+
if (extension) {
|
|
3124
|
+
out.push(
|
|
3125
|
+
`<text class="b" x="${coord(rect.x + 4)}" y="${coord(rect.y + 22.5)}" fill="${ink}">${escapeHtml(extension)}</text>`
|
|
3126
|
+
);
|
|
3127
|
+
}
|
|
3128
|
+
return;
|
|
3129
|
+
}
|
|
3130
|
+
const label = truncate(nodeName, available, 12.5);
|
|
3131
|
+
if (!label) return;
|
|
3132
|
+
out.push(
|
|
3133
|
+
`<text class="a" x="${coord(rect.x + 4)}" y="${coord(rect.y + 13)}" fill="${ink}">${escapeHtml(label)}</text>`
|
|
3134
|
+
);
|
|
3052
3135
|
}
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
"
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
];
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
}
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
files.map((file) => file.churn).filter((value) => value > 0),
|
|
3080
|
-
0.95
|
|
3136
|
+
function renderPlaced(placed, out, model, metricIndex, metric) {
|
|
3137
|
+
const node = placed.node;
|
|
3138
|
+
const rect = placed.rect;
|
|
3139
|
+
if (node.kind === "file") {
|
|
3140
|
+
const index = node.fileIndex;
|
|
3141
|
+
if (index === void 0) return;
|
|
3142
|
+
const file = model.files[index];
|
|
3143
|
+
if (!file) return;
|
|
3144
|
+
const value = metricIndex >= 0 ? file.values[metricIndex] ?? null : null;
|
|
3145
|
+
const fill = colorForFile(
|
|
3146
|
+
model.initialLens,
|
|
3147
|
+
file.risk,
|
|
3148
|
+
model.sections[file.section],
|
|
3149
|
+
value,
|
|
3150
|
+
metric
|
|
3151
|
+
);
|
|
3152
|
+
const ink = inkFor(fill);
|
|
3153
|
+
out.push(
|
|
3154
|
+
`<rect data-i="${index}" x="${coord(rect.x)}" y="${coord(rect.y)}" width="${span(rect.w)}" height="${span(rect.h)}" fill="${fill}"/>`
|
|
3155
|
+
);
|
|
3156
|
+
labelFor(file.name, rect, out, ink);
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
const folder = node;
|
|
3160
|
+
out.push(
|
|
3161
|
+
`<g class="folder" data-nm="${escapeAttr(folder.name)}" data-n="${folder.fileCount}" data-a="${coord(folder.value)}">`
|
|
3081
3162
|
);
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
0.95
|
|
3163
|
+
out.push(
|
|
3164
|
+
`<rect class="fr" x="${coord(rect.x)}" y="${coord(rect.y)}" width="${span(rect.w)}" height="${span(rect.h)}"/>`
|
|
3085
3165
|
);
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
const
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3166
|
+
if (placed.headerHeight >= 13 && rect.w >= 30) {
|
|
3167
|
+
const headerHeight = placed.headerHeight;
|
|
3168
|
+
out.push(
|
|
3169
|
+
`<rect class="fh" x="${coord(rect.x + 1)}" y="${coord(rect.y + 1)}" width="${span(rect.w - 2)}" height="${span(headerHeight - 1)}"/>`
|
|
3170
|
+
);
|
|
3171
|
+
const count = formatCompact(folder.value);
|
|
3172
|
+
const showCount = rect.w >= 86;
|
|
3173
|
+
const countWidth = showCount ? count.length * 5.8 + 12 : 0;
|
|
3174
|
+
const name = truncate(folder.name, Math.max(8, rect.w - 12 - countWidth), 11.5);
|
|
3175
|
+
if (name) {
|
|
3176
|
+
out.push(
|
|
3177
|
+
`<text class="ft" x="${coord(rect.x + 6)}" y="${coord(rect.y + headerHeight / 2 + 4)}">${escapeHtml(name)}</text>`
|
|
3178
|
+
);
|
|
3179
|
+
}
|
|
3180
|
+
if (showCount) {
|
|
3181
|
+
out.push(
|
|
3182
|
+
`<text class="fn" x="${coord(rect.x + rect.w - 6)}" y="${coord(rect.y + headerHeight / 2 + 3.6)}">${escapeHtml(count)}</text>`
|
|
3183
|
+
);
|
|
3184
|
+
}
|
|
3100
3185
|
}
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
function norm(value, p95) {
|
|
3104
|
-
return p95 > 0 ? clamp01(value / p95) : 0;
|
|
3105
|
-
}
|
|
3106
|
-
function percentile(values, p) {
|
|
3107
|
-
if (values.length === 0) return 0;
|
|
3108
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
3109
|
-
const index = Math.min(
|
|
3110
|
-
sorted.length - 1,
|
|
3111
|
-
Math.max(0, Math.floor((sorted.length - 1) * p))
|
|
3112
|
-
);
|
|
3113
|
-
return sorted[index] ?? 0;
|
|
3114
|
-
}
|
|
3115
|
-
function clamp01(value) {
|
|
3116
|
-
if (!Number.isFinite(value)) return 0;
|
|
3117
|
-
return Math.max(0, Math.min(1, value));
|
|
3118
|
-
}
|
|
3119
|
-
|
|
3120
|
-
// packages/render/src/model.ts
|
|
3121
|
-
var RenderError = class extends Error {
|
|
3122
|
-
constructor(message) {
|
|
3123
|
-
super(message);
|
|
3124
|
-
this.name = "RenderError";
|
|
3186
|
+
for (const child of placed.children) {
|
|
3187
|
+
renderPlaced(child, out, model, metricIndex, metric);
|
|
3125
3188
|
}
|
|
3126
|
-
|
|
3127
|
-
var PALETTE = [
|
|
3128
|
-
"#b9c4d0",
|
|
3129
|
-
"#c9b8a8",
|
|
3130
|
-
"#a9bfae",
|
|
3131
|
-
"#c4b6c9",
|
|
3132
|
-
"#c9c2a6",
|
|
3133
|
-
"#d5d2ca",
|
|
3134
|
-
"#c2c8d0",
|
|
3135
|
-
"#d6c6bd",
|
|
3136
|
-
"#b7c6c2",
|
|
3137
|
-
"#cbbfb0",
|
|
3138
|
-
"#bcc3b9",
|
|
3139
|
-
"#cfc6c9"
|
|
3140
|
-
];
|
|
3141
|
-
var CURATED_METRICS = [
|
|
3142
|
-
"rloc",
|
|
3143
|
-
"maxComplexity",
|
|
3144
|
-
"max_complexity_per_function",
|
|
3145
|
-
"complexity",
|
|
3146
|
-
"functions",
|
|
3147
|
-
"number_of_functions",
|
|
3148
|
-
"maxFunctionLength",
|
|
3149
|
-
"loc",
|
|
3150
|
-
"imports",
|
|
3151
|
-
"externalImports",
|
|
3152
|
-
"builtinImports",
|
|
3153
|
-
"typeOnlyImports",
|
|
3154
|
-
"unresolvedImports",
|
|
3155
|
-
"fanIn",
|
|
3156
|
-
"fanOut",
|
|
3157
|
-
"inCycle",
|
|
3158
|
-
"cycleSize",
|
|
3159
|
-
"dependencyLevel",
|
|
3160
|
-
"number_of_commits",
|
|
3161
|
-
"number_of_authors",
|
|
3162
|
-
"incoming_dependencies",
|
|
3163
|
-
"outgoing_dependencies"
|
|
3164
|
-
];
|
|
3165
|
-
function isRecord3(value) {
|
|
3166
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3167
|
-
}
|
|
3168
|
-
function valueOf(values, key) {
|
|
3169
|
-
if (!key || !values) return void 0;
|
|
3170
|
-
const value = values[key];
|
|
3171
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
3189
|
+
out.push("</g>");
|
|
3172
3190
|
}
|
|
3173
|
-
function
|
|
3174
|
-
|
|
3175
|
-
const
|
|
3176
|
-
const
|
|
3177
|
-
|
|
3178
|
-
|
|
3191
|
+
function renderSvg(model) {
|
|
3192
|
+
const metricIndex = model.initialMetric ? model.metrics.findIndex((metric2) => metric2.key === model.initialMetric) : -1;
|
|
3193
|
+
const metric = metricIndex >= 0 ? model.metrics[metricIndex] : void 0;
|
|
3194
|
+
const out = [];
|
|
3195
|
+
out.push(
|
|
3196
|
+
`<svg id="map" viewBox="0 0 ${VIEW_WIDTH} ${VIEW_HEIGHT}" preserveAspectRatio="xMidYMid meet" role="img" aria-label="Mapa de arquitectura de ${escapeAttr(model.meta.project)}">`
|
|
3179
3197
|
);
|
|
3180
|
-
|
|
3181
|
-
}
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
for (
|
|
3185
|
-
|
|
3186
|
-
hash = Math.imul(hash, 16777619);
|
|
3187
|
-
}
|
|
3188
|
-
return hash >>> 0;
|
|
3189
|
-
}
|
|
3190
|
-
function sectionForPath(path8) {
|
|
3191
|
-
const folders = path8.split("/").filter(Boolean).slice(0, -1);
|
|
3192
|
-
if (folders.length === 0) return "(ra\xEDz)";
|
|
3193
|
-
if (folders.length === 1) return folders[0];
|
|
3194
|
-
return folders.slice(0, 3).join("/");
|
|
3195
|
-
}
|
|
3196
|
-
function asEdges(value) {
|
|
3197
|
-
if (!Array.isArray(value)) return [];
|
|
3198
|
-
const edges = [];
|
|
3199
|
-
for (const item of value) {
|
|
3200
|
-
if (!isRecord3(item)) continue;
|
|
3201
|
-
if (typeof item.fromId !== "string" || typeof item.toId !== "string") continue;
|
|
3202
|
-
edges.push({
|
|
3203
|
-
fromId: item.fromId,
|
|
3204
|
-
toId: item.toId,
|
|
3205
|
-
...item.isCyclic === true ? { isCyclic: true } : {}
|
|
3206
|
-
});
|
|
3198
|
+
out.push(
|
|
3199
|
+
`<rect class="canvas" x="0" y="0" width="${VIEW_WIDTH}" height="${VIEW_HEIGHT}"/>`
|
|
3200
|
+
);
|
|
3201
|
+
out.push(`<g id="cells">`);
|
|
3202
|
+
for (const placed of layoutMap(model.roots)) {
|
|
3203
|
+
renderPlaced(placed, out, model, metricIndex, metric);
|
|
3207
3204
|
}
|
|
3208
|
-
|
|
3205
|
+
out.push(`</g><g id="edges"></g></svg>`);
|
|
3206
|
+
return out.join("");
|
|
3209
3207
|
}
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3208
|
+
|
|
3209
|
+
// packages/render/src/html.ts
|
|
3210
|
+
var STYLES = readFileSync(new URL("./styles.css", import.meta.url), "utf8");
|
|
3211
|
+
var CLIENT = readFileSync(new URL("./client.js", import.meta.url), "utf8").replace(
|
|
3212
|
+
/<\/script/gi,
|
|
3213
|
+
"<\\/script"
|
|
3214
|
+
);
|
|
3215
|
+
function round(value, digits = 2) {
|
|
3216
|
+
const factor = 10 ** digits;
|
|
3217
|
+
return Math.round(value * factor) / factor;
|
|
3216
3218
|
}
|
|
3217
|
-
function
|
|
3218
|
-
|
|
3219
|
-
if (isRecord3(descriptor) && typeof descriptor.direction === "number") {
|
|
3220
|
-
return descriptor.direction;
|
|
3221
|
-
}
|
|
3222
|
-
return 0;
|
|
3219
|
+
function jsonForScript(value) {
|
|
3220
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
3223
3221
|
}
|
|
3224
|
-
function
|
|
3225
|
-
const
|
|
3226
|
-
const
|
|
3227
|
-
const
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
if (
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
}
|
|
3240
|
-
const complexityKey = firstMetric(coverage, COMPLEXITY_METRIC_CANDIDATES);
|
|
3241
|
-
const churnKey = firstMetric(coverage, CHURN_METRIC_CANDIDATES);
|
|
3242
|
-
const metricKeys = [];
|
|
3243
|
-
const addMetric = (key) => {
|
|
3244
|
-
if (key && coverage.has(key) && !metricKeys.includes(key)) metricKeys.push(key);
|
|
3245
|
-
};
|
|
3246
|
-
for (const key of CURATED_METRICS) addMetric(key);
|
|
3247
|
-
addMetric(areaMetric);
|
|
3248
|
-
addMetric(complexityKey);
|
|
3249
|
-
addMetric(churnKey);
|
|
3250
|
-
if (options.color && options.color !== "risk" && options.color !== "structure") {
|
|
3251
|
-
addMetric(options.color);
|
|
3252
|
-
}
|
|
3253
|
-
const singleRoot = cc.files.length === 1 && cc.files[0]?.type === "Folder" ? cc.files[0] : null;
|
|
3254
|
-
const prefix = singleRoot ? `${singleRoot.name}/` : "";
|
|
3255
|
-
const rootName = singleRoot?.name ?? "";
|
|
3256
|
-
const warnings = [];
|
|
3257
|
-
const files = [];
|
|
3258
|
-
const fileIndexById = /* @__PURE__ */ new Map();
|
|
3259
|
-
walkFiles(cc.files, (entry) => {
|
|
3260
|
-
const fullPath = entry.path;
|
|
3261
|
-
const path8 = prefix && fullPath.startsWith(prefix) ? fullPath.slice(prefix.length) : fullPath;
|
|
3262
|
-
const values = attributes[entry.node.id];
|
|
3263
|
-
const rawArea = valueOf(values, areaMetric);
|
|
3264
|
-
const index = files.length;
|
|
3265
|
-
fileIndexById.set(entry.node.id, index);
|
|
3266
|
-
files.push({
|
|
3267
|
-
index,
|
|
3268
|
-
nodeId: entry.node.id,
|
|
3269
|
-
path: path8,
|
|
3270
|
-
name: entry.node.name,
|
|
3271
|
-
section: -1,
|
|
3272
|
-
area: Math.max(1, rawArea ?? 0),
|
|
3273
|
-
actualArea: rawArea ?? 0,
|
|
3274
|
-
risk: 0,
|
|
3275
|
-
complexity: valueOf(values, complexityKey) ?? 0,
|
|
3276
|
-
churn: valueOf(values, churnKey) ?? 0,
|
|
3277
|
-
values: []
|
|
3278
|
-
});
|
|
3279
|
-
});
|
|
3280
|
-
if (areaMetric && files.length > 0) {
|
|
3281
|
-
const missing = files.filter((file) => file.actualArea === 0).length;
|
|
3282
|
-
if (missing > 0) {
|
|
3283
|
-
warnings.push(
|
|
3284
|
-
`${missing} ficheros sin "${areaMetric}"; se dibujan con \xE1rea m\xEDnima para no desaparecer`
|
|
3285
|
-
);
|
|
3286
|
-
}
|
|
3287
|
-
if (!options.areaMetric && areaMetric !== "rloc") {
|
|
3288
|
-
warnings.push(`"rloc" no est\xE1 presente; se usa "${areaMetric}" como \xE1rea`);
|
|
3289
|
-
}
|
|
3290
|
-
}
|
|
3291
|
-
const risks = riskScores(
|
|
3292
|
-
files.map((file) => ({
|
|
3293
|
-
complexity: file.complexity,
|
|
3294
|
-
churn: file.churn,
|
|
3295
|
-
size: file.actualArea
|
|
3296
|
-
}))
|
|
3297
|
-
);
|
|
3298
|
-
for (const [index, file] of files.entries()) {
|
|
3299
|
-
file.risk = risks[index] ?? 0;
|
|
3300
|
-
}
|
|
3301
|
-
const sectionAccumulator = /* @__PURE__ */ new Map();
|
|
3302
|
-
for (const file of files) {
|
|
3303
|
-
const key = sectionForPath(file.path);
|
|
3304
|
-
const current = sectionAccumulator.get(key) ?? { files: 0, area: 0 };
|
|
3305
|
-
current.files++;
|
|
3306
|
-
current.area += file.actualArea;
|
|
3307
|
-
sectionAccumulator.set(key, current);
|
|
3308
|
-
}
|
|
3309
|
-
const sections = [...sectionAccumulator.entries()].map(([key, value]) => ({
|
|
3310
|
-
key,
|
|
3311
|
-
label: key,
|
|
3312
|
-
files: value.files,
|
|
3313
|
-
area: value.area,
|
|
3314
|
-
color: PALETTE[hashString(key) % PALETTE.length]
|
|
3315
|
-
})).sort((a, b) => b.area - a.area || a.key.localeCompare(b.key));
|
|
3316
|
-
const sectionIndex = new Map(sections.map((section, index) => [section.key, index]));
|
|
3317
|
-
for (const file of files) {
|
|
3318
|
-
file.section = sectionIndex.get(sectionForPath(file.path)) ?? 0;
|
|
3319
|
-
}
|
|
3320
|
-
const metrics = metricKeys.map((key) => {
|
|
3321
|
-
const present = files.map((file) => valueOf(attributes[file.nodeId], key)).filter((value) => value !== void 0);
|
|
3322
|
-
const min = present.length > 0 ? Math.min(...present) : 0;
|
|
3323
|
-
const actualMax = present.length > 0 ? Math.max(...present) : 0;
|
|
3324
|
-
const p95 = percentile2(present, 0.95);
|
|
3325
|
-
const skewed = min >= 0 && p95 > 0 && actualMax > p95 * 4;
|
|
3326
|
-
const max = skewed ? actualMax : p95 > 0 ? p95 : actualMax;
|
|
3327
|
-
return {
|
|
3328
|
-
key,
|
|
3329
|
-
title: metricTitle(descriptors, key),
|
|
3330
|
-
direction: metricDirection(descriptors, key),
|
|
3331
|
-
min,
|
|
3332
|
-
max,
|
|
3333
|
-
actualMax,
|
|
3334
|
-
p95,
|
|
3335
|
-
mode: skewed ? "log" : "linear",
|
|
3336
|
-
coverage: present.length
|
|
3337
|
-
};
|
|
3338
|
-
});
|
|
3339
|
-
for (const file of files) {
|
|
3340
|
-
const values = attributes[file.nodeId];
|
|
3341
|
-
file.values = metrics.map((metric) => valueOf(values, metric.key) ?? null);
|
|
3342
|
-
}
|
|
3343
|
-
const roots = buildRoots(cc, singleRoot, fileIndexById, files);
|
|
3344
|
-
const folderCount = countFolders(roots);
|
|
3345
|
-
const dependency = cc.lenses?.dependency;
|
|
3346
|
-
const leafEdges = asEdges(dependency?.leafEdges);
|
|
3347
|
-
const rawEdges = leafEdges.length > 0 ? leafEdges : asEdges(dependency?.edges);
|
|
3348
|
-
const edgeSet = /* @__PURE__ */ new Set();
|
|
3349
|
-
const edges = [];
|
|
3350
|
-
const n = files.length;
|
|
3351
|
-
for (const edge of rawEdges) {
|
|
3352
|
-
const from = fileIndexById.get(edge.fromId);
|
|
3353
|
-
const to = fileIndexById.get(edge.toId);
|
|
3354
|
-
if (from === void 0 || to === void 0 || from === to) continue;
|
|
3355
|
-
const key = from * n + to;
|
|
3356
|
-
if (edgeSet.has(key)) continue;
|
|
3357
|
-
edgeSet.add(key);
|
|
3358
|
-
edges.push({ from, to });
|
|
3359
|
-
if (edges.length >= 2e4) {
|
|
3360
|
-
warnings.push("se han limitado las dependencias a 20.000 aristas");
|
|
3361
|
-
break;
|
|
3362
|
-
}
|
|
3363
|
-
}
|
|
3364
|
-
edges.sort((a, b) => a.from - b.from || a.to - b.to);
|
|
3365
|
-
const flaggedEdges = /* @__PURE__ */ new Set();
|
|
3366
|
-
for (const edge of rawEdges) {
|
|
3367
|
-
if (edge.isCyclic !== true) continue;
|
|
3368
|
-
const from = fileIndexById.get(edge.fromId);
|
|
3369
|
-
const to = fileIndexById.get(edge.toId);
|
|
3370
|
-
if (from === void 0 || to === void 0) continue;
|
|
3371
|
-
flaggedEdges.add(from * n + to);
|
|
3372
|
-
}
|
|
3373
|
-
const cycleSet = /* @__PURE__ */ new Set();
|
|
3374
|
-
const cycles = [];
|
|
3375
|
-
for (const edge of edges) {
|
|
3376
|
-
const key = edge.from * n + edge.to;
|
|
3377
|
-
const backward = edgeSet.has(edge.to * n + edge.from);
|
|
3378
|
-
if (backward || flaggedEdges.has(key)) {
|
|
3379
|
-
if (!cycleSet.has(key)) {
|
|
3380
|
-
cycleSet.add(key);
|
|
3381
|
-
cycles.push(edge);
|
|
3222
|
+
function serialise(model) {
|
|
3223
|
+
const directoryIndex = /* @__PURE__ */ new Map();
|
|
3224
|
+
const directories = [];
|
|
3225
|
+
const fileRows = model.files.map((file) => {
|
|
3226
|
+
const slash = file.path.lastIndexOf("/");
|
|
3227
|
+
const directory = slash >= 0 ? file.path.slice(0, slash) : "";
|
|
3228
|
+
let dirIndex = -1;
|
|
3229
|
+
if (directory) {
|
|
3230
|
+
const known = directoryIndex.get(directory);
|
|
3231
|
+
if (known === void 0) {
|
|
3232
|
+
dirIndex = directories.length;
|
|
3233
|
+
directoryIndex.set(directory, dirIndex);
|
|
3234
|
+
directories.push(directory);
|
|
3235
|
+
} else {
|
|
3236
|
+
dirIndex = known;
|
|
3382
3237
|
}
|
|
3383
3238
|
}
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
throw new RenderError(`la m\xE9trica de color "${options.color}" no existe en este cc.json`);
|
|
3398
|
-
}
|
|
3399
|
-
initialLens = "metric";
|
|
3400
|
-
initialMetric = options.color;
|
|
3401
|
-
}
|
|
3402
|
-
const defaultSelection = pickDefaultSelection(files, initialLens, initialMetric, metrics);
|
|
3403
|
-
const project = cc.meta.projectName || options.title || "Proyecto sin nombre";
|
|
3239
|
+
const name = slash >= 0 ? file.path.slice(slash + 1) : file.path;
|
|
3240
|
+
return [
|
|
3241
|
+
dirIndex,
|
|
3242
|
+
name,
|
|
3243
|
+
file.section,
|
|
3244
|
+
round(file.area, 1),
|
|
3245
|
+
round(file.actualArea, 1),
|
|
3246
|
+
round(file.risk, 4),
|
|
3247
|
+
round(file.complexity, 1),
|
|
3248
|
+
round(file.churn, 1),
|
|
3249
|
+
...file.values.map((value) => value === null ? null : round(value, 2))
|
|
3250
|
+
];
|
|
3251
|
+
});
|
|
3404
3252
|
return {
|
|
3253
|
+
v: 1,
|
|
3405
3254
|
meta: {
|
|
3406
|
-
project,
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
rootName
|
|
3255
|
+
project: model.meta.project,
|
|
3256
|
+
commit: model.meta.commit,
|
|
3257
|
+
checksum: model.meta.checksum,
|
|
3258
|
+
api: model.meta.apiVersion,
|
|
3259
|
+
root: model.meta.rootName
|
|
3412
3260
|
},
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
fileIndex: index
|
|
3445
|
-
};
|
|
3446
|
-
}
|
|
3447
|
-
const children = (node.children ?? []).map(
|
|
3448
|
-
(child) => build(child, path8 ? `${path8}/${child.name}` : child.name)
|
|
3449
|
-
);
|
|
3450
|
-
const folder = {
|
|
3451
|
-
kind: "folder",
|
|
3452
|
-
key: `d:${path8}`,
|
|
3453
|
-
name: node.name,
|
|
3454
|
-
path: path8,
|
|
3455
|
-
value: children.reduce((sum, child) => sum + child.value, 0) || 1,
|
|
3456
|
-
fileCount: children.reduce((sum, child) => sum + child.fileCount, 0),
|
|
3457
|
-
section: -1,
|
|
3458
|
-
children
|
|
3459
|
-
};
|
|
3460
|
-
while (folder.children?.length === 1 && folder.children[0]?.kind === "folder") {
|
|
3461
|
-
const only = folder.children[0];
|
|
3462
|
-
folder.name = `${folder.name}/${only.name}`;
|
|
3463
|
-
folder.path = only.path;
|
|
3464
|
-
folder.key = `d:${only.path}`;
|
|
3465
|
-
folder.value = only.value;
|
|
3466
|
-
folder.fileCount = only.fileCount;
|
|
3467
|
-
folder.children = only.children;
|
|
3468
|
-
}
|
|
3469
|
-
return folder;
|
|
3261
|
+
area: {
|
|
3262
|
+
key: model.areaMetric,
|
|
3263
|
+
title: model.areaTitle,
|
|
3264
|
+
total: round(model.totalArea, 1)
|
|
3265
|
+
},
|
|
3266
|
+
lens: model.initialLens,
|
|
3267
|
+
metric: model.initialMetric,
|
|
3268
|
+
metrics: model.metrics.map((metric) => ({
|
|
3269
|
+
key: metric.key,
|
|
3270
|
+
title: metric.title,
|
|
3271
|
+
min: round(metric.min, 2),
|
|
3272
|
+
max: round(metric.max, 2),
|
|
3273
|
+
mode: metric.mode,
|
|
3274
|
+
dir: metric.direction,
|
|
3275
|
+
coverage: metric.coverage
|
|
3276
|
+
})),
|
|
3277
|
+
sections: model.sections.map((section) => ({
|
|
3278
|
+
key: section.key,
|
|
3279
|
+
color: section.color,
|
|
3280
|
+
files: section.files,
|
|
3281
|
+
area: round(section.area, 1)
|
|
3282
|
+
})),
|
|
3283
|
+
dirs: directories,
|
|
3284
|
+
files: fileRows,
|
|
3285
|
+
edges: model.edges.flatMap((edge) => [edge.from, edge.to]),
|
|
3286
|
+
cycles: model.cycles.flatMap((edge) => [edge.from, edge.to]),
|
|
3287
|
+
hot: model.hotspotCount,
|
|
3288
|
+
default: model.defaultSelection,
|
|
3289
|
+
fileCount: model.fileCount,
|
|
3290
|
+
folderCount: model.folderCount,
|
|
3291
|
+
totalArea: round(model.totalArea, 1)
|
|
3470
3292
|
};
|
|
3471
|
-
if (singleRoot) {
|
|
3472
|
-
return (singleRoot.children ?? []).map((child) => build(child, child.name));
|
|
3473
|
-
}
|
|
3474
|
-
return cc.files.map((root) => build(root, root.name));
|
|
3475
3293
|
}
|
|
3476
|
-
function
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3294
|
+
function renderLensButtons(model) {
|
|
3295
|
+
const lenses = [
|
|
3296
|
+
{ id: "structure", label: "Estructura" },
|
|
3297
|
+
{ id: "risk", label: "Riesgo" }
|
|
3298
|
+
];
|
|
3299
|
+
if (model.metrics.length > 0) lenses.push({ id: "metric", label: "M\xE9trica" });
|
|
3300
|
+
return lenses.map(
|
|
3301
|
+
(lens) => `<button type="button" class="lens" role="tab" data-lens="${lens.id}" aria-selected="${String(
|
|
3302
|
+
model.initialLens === lens.id
|
|
3303
|
+
)}">${lens.label}</button>`
|
|
3304
|
+
).join("");
|
|
3485
3305
|
}
|
|
3486
|
-
function
|
|
3487
|
-
|
|
3488
|
-
if (
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
if (value === null || value === void 0) continue;
|
|
3495
|
-
if (!best || value > (best.values[index] ?? -Infinity)) best = file;
|
|
3496
|
-
}
|
|
3497
|
-
if (best) return best.index;
|
|
3498
|
-
}
|
|
3306
|
+
function renderFindings(model) {
|
|
3307
|
+
const chips = [];
|
|
3308
|
+
if (model.hotspotCount > 0) {
|
|
3309
|
+
chips.push(
|
|
3310
|
+
`<button type="button" class="finding" data-focus="hotspot"><i class="dot info"></i><b>${formatInt(
|
|
3311
|
+
model.hotspotCount
|
|
3312
|
+
)}</b> hotspots</button>`
|
|
3313
|
+
);
|
|
3499
3314
|
}
|
|
3500
|
-
if (
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3315
|
+
if (model.cycles.length > 0) {
|
|
3316
|
+
chips.push(
|
|
3317
|
+
`<button type="button" class="finding" data-focus="cycle"><i class="dot warn"></i><b>${formatInt(
|
|
3318
|
+
model.cycles.length
|
|
3319
|
+
)}</b> aristas c\xEDclicas</button>`
|
|
3320
|
+
);
|
|
3504
3321
|
}
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
[0.5, "#b9c9cf"],
|
|
3521
|
-
[1, "#355b6b"]
|
|
3522
|
-
];
|
|
3523
|
-
var RISK_GRADIENT = "linear-gradient(90deg, #ebe7df 0%, #e9cd98 35%, #d68a54 65%, #a42e2c 100%)";
|
|
3524
|
-
var METRIC_GRADIENT = "linear-gradient(90deg, #ebe7df 0%, #b9c9cf 50%, #355b6b 100%)";
|
|
3525
|
-
function clamp012(value) {
|
|
3526
|
-
if (!Number.isFinite(value)) return 0;
|
|
3527
|
-
return Math.max(0, Math.min(1, value));
|
|
3528
|
-
}
|
|
3529
|
-
function hexToRgb(hex) {
|
|
3530
|
-
const value = hex.replace("#", "");
|
|
3531
|
-
return [
|
|
3532
|
-
Number.parseInt(value.slice(0, 2), 16),
|
|
3533
|
-
Number.parseInt(value.slice(2, 4), 16),
|
|
3534
|
-
Number.parseInt(value.slice(4, 6), 16)
|
|
3535
|
-
];
|
|
3536
|
-
}
|
|
3537
|
-
function rgbToHex(rgb) {
|
|
3538
|
-
return `#${rgb.map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
|
|
3322
|
+
chips.push(
|
|
3323
|
+
`<span class="finding static"><i class="dot neutral"></i><b>${formatInt(
|
|
3324
|
+
model.fileCount
|
|
3325
|
+
)}</b> ficheros</span>`
|
|
3326
|
+
);
|
|
3327
|
+
chips.push(
|
|
3328
|
+
`<span class="finding static"><i class="dot neutral"></i><b>${formatInt(
|
|
3329
|
+
model.edges.length
|
|
3330
|
+
)}</b> dependencias</span>`
|
|
3331
|
+
);
|
|
3332
|
+
return `${chips.join("")}<span class="spacer"></span><span id="search-status" class="search-status" aria-live="polite"></span><span class="run-meta">${escapeHtml(
|
|
3333
|
+
model.areaTitle
|
|
3334
|
+
)} \xB7 ${formatInt(model.metrics.length)} m\xE9tricas \xB7 cc.json ${escapeHtml(
|
|
3335
|
+
model.meta.apiVersion
|
|
3336
|
+
)}</span>`;
|
|
3539
3337
|
}
|
|
3540
|
-
function
|
|
3541
|
-
|
|
3542
|
-
const to = hexToRgb(b);
|
|
3543
|
-
return rgbToHex([
|
|
3544
|
-
Math.round(from[0] + (to[0] - from[0]) * t),
|
|
3545
|
-
Math.round(from[1] + (to[1] - from[1]) * t),
|
|
3546
|
-
Math.round(from[2] + (to[2] - from[2]) * t)
|
|
3547
|
-
]);
|
|
3338
|
+
function metricByKey(model, key) {
|
|
3339
|
+
return key ? model.metrics.find((metric) => metric.key === key) : void 0;
|
|
3548
3340
|
}
|
|
3549
|
-
function
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3341
|
+
function renderLegend(model) {
|
|
3342
|
+
if (model.initialLens === "structure") {
|
|
3343
|
+
const items = model.sections.slice(0, 6).map(
|
|
3344
|
+
(section) => `<span><i style="background:${section.color}"></i>${escapeHtml(section.key)}</span>`
|
|
3345
|
+
).join("");
|
|
3346
|
+
return `<span>Carpeta</span><span class="sws">${items}</span>`;
|
|
3347
|
+
}
|
|
3348
|
+
if (model.initialLens === "metric" && model.initialMetric) {
|
|
3349
|
+
const metric = metricByKey(model, model.initialMetric);
|
|
3350
|
+
if (metric) {
|
|
3351
|
+
const direction = metric.direction === -1 ? " \xB7 menos es mejor" : metric.direction === 1 ? " \xB7 m\xE1s es mejor" : "";
|
|
3352
|
+
return `<span>${escapeHtml(metric.title)}${direction}</span><span class="mono">${escapeHtml(
|
|
3353
|
+
formatCompact(metric.min)
|
|
3354
|
+
)}</span><span class="bar" style="background:${METRIC_GRADIENT}"></span><span class="mono">${escapeHtml(
|
|
3355
|
+
formatCompact(metric.max)
|
|
3356
|
+
)}</span>`;
|
|
3557
3357
|
}
|
|
3558
3358
|
}
|
|
3559
|
-
return
|
|
3359
|
+
return `<span>Riesgo (0\u2013100)</span><span class="mono">0</span><span class="bar" style="background:${RISK_GRADIENT}"></span><span class="mono">100</span>`;
|
|
3560
3360
|
}
|
|
3561
|
-
function
|
|
3562
|
-
return
|
|
3361
|
+
function renderMetricOptions(model) {
|
|
3362
|
+
return model.metrics.map(
|
|
3363
|
+
(metric) => `<option value="${escapeAttr(metric.key)}"${metric.key === model.initialMetric ? " selected" : ""}>${escapeHtml(metric.title)}</option>`
|
|
3364
|
+
).join("");
|
|
3563
3365
|
}
|
|
3564
|
-
function
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
}
|
|
3571
|
-
|
|
3572
|
-
|
|
3366
|
+
function renderInspectorSummary(model) {
|
|
3367
|
+
const hotspots = [...model.files].sort((a, b) => b.risk - a.risk || a.path.localeCompare(b.path)).slice(0, 5);
|
|
3368
|
+
return `<div class="summary">
|
|
3369
|
+
<div class="sub">Proyecto</div>
|
|
3370
|
+
<div class="big">${formatInt(model.fileCount)}</div>
|
|
3371
|
+
<div class="sub">ficheros \xB7 ${formatInt(model.folderCount)} carpetas</div>
|
|
3372
|
+
<p>${formatInt(model.totalArea)} de ${escapeHtml(
|
|
3373
|
+
model.areaTitle.toLowerCase()
|
|
3374
|
+
)} \xB7 ${formatInt(model.metrics.length)} m\xE9tricas \xB7 ${formatInt(
|
|
3375
|
+
model.hotspotCount
|
|
3376
|
+
)} hotspots.</p>
|
|
3377
|
+
<h4>Mayor riesgo</h4>
|
|
3378
|
+
<ul class="deps">${hotspots.map(
|
|
3379
|
+
(file) => `<li data-file="${file.index}"><i class="sw" style="background:${riskColor(
|
|
3380
|
+
file.risk
|
|
3381
|
+
)}"></i><span>${escapeHtml(file.path)}</span><em>${Math.round(
|
|
3382
|
+
file.risk * 100
|
|
3383
|
+
)}</em></li>`
|
|
3384
|
+
).join("")}</ul>
|
|
3385
|
+
<p>Selecciona un bloque para ver sus m\xE9tricas y sus dependencias. Clic en la cabecera de una carpeta para enfocarla.</p>
|
|
3386
|
+
</div>`;
|
|
3387
|
+
}
|
|
3388
|
+
function renderHtml(model) {
|
|
3389
|
+
const project = escapeHtml(model.meta.project);
|
|
3390
|
+
const title = escapeHtml(model.meta.title);
|
|
3391
|
+
const commit = model.meta.commit ? model.meta.commit.slice(0, 7) : "";
|
|
3392
|
+
const checksum = model.meta.checksum ? model.meta.checksum.slice(0, 8) : "";
|
|
3393
|
+
const badge = commit || checksum || `cc.json ${model.meta.apiVersion}`;
|
|
3394
|
+
const payload = jsonForScript(serialise(model));
|
|
3395
|
+
const metricPicker = model.metrics.length > 0 ? `<label class="metric-pick" id="metric-pick"${model.initialLens === "metric" ? "" : " hidden"}><span>M\xE9trica</span><select id="metric-select">${renderMetricOptions(
|
|
3396
|
+
model
|
|
3397
|
+
)}</select></label>` : "";
|
|
3398
|
+
const crumbs = model.meta.rootName ? `${project} <span class="sep">/</span> ${escapeHtml(model.meta.rootName)}` : project;
|
|
3399
|
+
return `<!doctype html>
|
|
3400
|
+
<html lang="es">
|
|
3401
|
+
<head>
|
|
3402
|
+
<meta charset="utf-8">
|
|
3403
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
3404
|
+
<meta name="color-scheme" content="light">
|
|
3405
|
+
<meta name="generator" content="CodArch">
|
|
3406
|
+
<title>CodArch \xB7 ${title}</title>
|
|
3407
|
+
<style>${STYLES}</style>
|
|
3408
|
+
</head>
|
|
3409
|
+
<body>
|
|
3410
|
+
<div class="app">
|
|
3411
|
+
<header>
|
|
3412
|
+
<div class="brand">CodArch</div>
|
|
3413
|
+
<div class="repo">
|
|
3414
|
+
<b>${title}</b>
|
|
3415
|
+
${model.meta.rootName && model.meta.rootName !== model.meta.project ? `<span>${escapeHtml(model.meta.rootName)}</span>` : ""}
|
|
3416
|
+
<span class="commit">${escapeHtml(badge)}</span>
|
|
3417
|
+
</div>
|
|
3418
|
+
<div class="spacer"></div>
|
|
3419
|
+
<nav class="lenses" role="tablist" aria-label="Lentes">${renderLensButtons(model)}</nav>
|
|
3420
|
+
${metricPicker}
|
|
3421
|
+
<label class="search">
|
|
3422
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
3423
|
+
<circle cx="7" cy="7" r="4.5" stroke="#8d877d" stroke-width="1.4"/>
|
|
3424
|
+
<path d="M10.5 10.5 14 14" stroke="#8d877d" stroke-width="1.4"/>
|
|
3425
|
+
</svg>
|
|
3426
|
+
<input id="q" type="search" placeholder="Buscar fichero\u2026" autocomplete="off" aria-label="Buscar fichero">
|
|
3427
|
+
</label>
|
|
3428
|
+
</header>
|
|
3429
|
+
|
|
3430
|
+
<div class="findings">${renderFindings(model)}</div>
|
|
3431
|
+
|
|
3432
|
+
<main class="map-wrap">
|
|
3433
|
+
<div class="map" id="map">${renderSvg(model)}</div>
|
|
3434
|
+
<div class="tooltip" id="tooltip"></div>
|
|
3435
|
+
</main>
|
|
3436
|
+
|
|
3437
|
+
<aside id="inspector">${renderInspectorSummary(model)}</aside>
|
|
3438
|
+
|
|
3439
|
+
<footer>
|
|
3440
|
+
<span class="crumbs" id="crumbs">${crumbs}</span>
|
|
3441
|
+
<span class="hint">clic para fijar \xB7 esc para limpiar</span>
|
|
3442
|
+
<span class="legend" id="legend">${renderLegend(model)}</span>
|
|
3443
|
+
</footer>
|
|
3444
|
+
</div>
|
|
3445
|
+
<script id="codarch-data" type="application/json">${payload}</script>
|
|
3446
|
+
<script>${CLIENT}</script>
|
|
3447
|
+
</body>
|
|
3448
|
+
</html>`;
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
// apps/cli/src/io.ts
|
|
3452
|
+
import fs5 from "node:fs";
|
|
3453
|
+
import zlib from "node:zlib";
|
|
3454
|
+
function readJsonFile2(file) {
|
|
3455
|
+
const buffer = fs5.readFileSync(file);
|
|
3456
|
+
const isGzip = buffer.length > 2 && buffer[0] === 31 && buffer[1] === 139;
|
|
3457
|
+
const raw = isGzip ? zlib.gunzipSync(buffer) : buffer;
|
|
3458
|
+
const text = raw.toString("utf8");
|
|
3459
|
+
try {
|
|
3460
|
+
return { json: JSON.parse(text), bytes: buffer.length };
|
|
3461
|
+
} catch (error) {
|
|
3462
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3463
|
+
throw new Error(`no es JSON v\xE1lido: ${message}`);
|
|
3573
3464
|
}
|
|
3574
|
-
return ramp(METRIC_STOPS, t);
|
|
3575
|
-
}
|
|
3576
|
-
function inkFor(color) {
|
|
3577
|
-
const [r, g, b] = hexToRgb(color);
|
|
3578
|
-
const luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
|
|
3579
|
-
return luminance > 0.52 ? "#1a1a1a" : "#f7f5f1";
|
|
3580
3465
|
}
|
|
3581
|
-
function
|
|
3582
|
-
if (
|
|
3583
|
-
if (
|
|
3584
|
-
return
|
|
3466
|
+
function formatBytes(bytes) {
|
|
3467
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
3468
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
3469
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
3585
3470
|
}
|
|
3586
3471
|
|
|
3587
|
-
//
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
const
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3472
|
+
// apps/cli/src/commands/analyze.ts
|
|
3473
|
+
var numberFormat = new Intl.NumberFormat("es-ES");
|
|
3474
|
+
function num(value) {
|
|
3475
|
+
return numberFormat.format(value);
|
|
3476
|
+
}
|
|
3477
|
+
function field(label, value) {
|
|
3478
|
+
return ` ${label.padEnd(12)} ${value}`;
|
|
3479
|
+
}
|
|
3480
|
+
function runAnalyze(options) {
|
|
3481
|
+
const root = options.root ?? ".";
|
|
3482
|
+
let result;
|
|
3483
|
+
try {
|
|
3484
|
+
result = analyzeRepository(root, {
|
|
3485
|
+
ignore: options.ignore,
|
|
3486
|
+
projectName: options.projectName,
|
|
3487
|
+
git: options.git
|
|
3488
|
+
});
|
|
3489
|
+
} catch (error) {
|
|
3490
|
+
console.error(`no se pudo analizar ${root}: ${error.message}`);
|
|
3491
|
+
return 1;
|
|
3492
|
+
}
|
|
3493
|
+
let cc;
|
|
3494
|
+
try {
|
|
3495
|
+
cc = parseCcJson(result.snapshot);
|
|
3496
|
+
} catch (error) {
|
|
3497
|
+
if (error instanceof CcJsonError) {
|
|
3498
|
+
console.error(`el snapshot generado no es cc.json v\xE1lido: ${error.message}`);
|
|
3499
|
+
return 1;
|
|
3613
3500
|
}
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3501
|
+
throw error;
|
|
3502
|
+
}
|
|
3503
|
+
const outDirectory = path5.resolve(
|
|
3504
|
+
options.out ?? path5.join(path5.resolve(root), ".codarch")
|
|
3505
|
+
);
|
|
3506
|
+
const snapshotPath = path5.join(outDirectory, "snapshot.cc.json");
|
|
3507
|
+
const mapPath = path5.join(outDirectory, "map.html");
|
|
3508
|
+
try {
|
|
3509
|
+
fs6.mkdirSync(outDirectory, { recursive: true });
|
|
3510
|
+
fs6.writeFileSync(
|
|
3511
|
+
snapshotPath,
|
|
3512
|
+
`${JSON.stringify(cc, null, 2)}
|
|
3513
|
+
`,
|
|
3514
|
+
"utf8"
|
|
3618
3515
|
);
|
|
3516
|
+
} catch (error) {
|
|
3517
|
+
console.error(
|
|
3518
|
+
`no se pudo escribir ${snapshotPath}: ${error.message}`
|
|
3519
|
+
);
|
|
3520
|
+
return 1;
|
|
3619
3521
|
}
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
y += thickness;
|
|
3635
|
-
h -= thickness;
|
|
3636
|
-
} else {
|
|
3637
|
-
if (h <= 0) return;
|
|
3638
|
-
const thickness = sum / h;
|
|
3639
|
-
if (!Number.isFinite(thickness) || thickness <= 0) return;
|
|
3640
|
-
let position = y;
|
|
3641
|
-
for (const item of current) {
|
|
3642
|
-
const length = area(item) / thickness;
|
|
3643
|
-
placed.push({ item, x, y: position, w: thickness, h: length });
|
|
3644
|
-
position += length;
|
|
3645
|
-
}
|
|
3646
|
-
x += thickness;
|
|
3647
|
-
w -= thickness;
|
|
3522
|
+
let mapBytes = 0;
|
|
3523
|
+
let mapError = null;
|
|
3524
|
+
if (result.files > 0) {
|
|
3525
|
+
try {
|
|
3526
|
+
const model = buildRenderModel(cc, {
|
|
3527
|
+
areaMetric: "rloc",
|
|
3528
|
+
color: "risk",
|
|
3529
|
+
title: options.title
|
|
3530
|
+
});
|
|
3531
|
+
const html = renderHtml(model);
|
|
3532
|
+
fs6.writeFileSync(mapPath, html, "utf8");
|
|
3533
|
+
mapBytes = Buffer.byteLength(html, "utf8");
|
|
3534
|
+
} catch (error) {
|
|
3535
|
+
mapError = error instanceof RenderError ? error.message : error.message;
|
|
3648
3536
|
}
|
|
3537
|
+
} else {
|
|
3538
|
+
mapError = "no hay ficheros TypeScript/JavaScript que dibujar";
|
|
3649
3539
|
}
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
}
|
|
3664
|
-
place(row, horizontal);
|
|
3665
|
-
row = [];
|
|
3666
|
-
}
|
|
3540
|
+
const snapshotBytes = Buffer.byteLength(
|
|
3541
|
+
fs6.readFileSync(snapshotPath),
|
|
3542
|
+
"utf8"
|
|
3543
|
+
);
|
|
3544
|
+
const lines = [];
|
|
3545
|
+
lines.push("");
|
|
3546
|
+
lines.push(" CodArch \xB7 an\xE1lisis");
|
|
3547
|
+
lines.push("");
|
|
3548
|
+
lines.push(field("ra\xEDz", path5.resolve(root)));
|
|
3549
|
+
lines.push(field("snapshot", `${snapshotPath} (${formatBytes(snapshotBytes)})`));
|
|
3550
|
+
if (mapError) {
|
|
3551
|
+
lines.push(field("mapa", `omitido (${mapError})`));
|
|
3552
|
+
} else {
|
|
3553
|
+
lines.push(field("mapa", `${mapPath} (${formatBytes(mapBytes)})`));
|
|
3667
3554
|
}
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
var VIEW_PADDING = 8;
|
|
3676
|
-
var MAX_DEPTH3 = 12;
|
|
3677
|
-
function layoutMap(roots) {
|
|
3678
|
-
const rect = {
|
|
3679
|
-
x: VIEW_PADDING,
|
|
3680
|
-
y: VIEW_PADDING,
|
|
3681
|
-
w: VIEW_WIDTH - VIEW_PADDING * 2,
|
|
3682
|
-
h: VIEW_HEIGHT - VIEW_PADDING * 2
|
|
3683
|
-
};
|
|
3684
|
-
return layoutChildren(roots, rect, 0);
|
|
3685
|
-
}
|
|
3686
|
-
function layoutChildren(nodes, rect, depth) {
|
|
3687
|
-
if (nodes.length === 0 || rect.w < 2 || rect.h < 2) return [];
|
|
3688
|
-
const items = nodes.map((node) => ({
|
|
3689
|
-
key: node.key,
|
|
3690
|
-
value: Math.max(1, node.value),
|
|
3691
|
-
node
|
|
3692
|
-
}));
|
|
3693
|
-
return squarify(items, rect).map(
|
|
3694
|
-
(placed) => layoutNode(placed.item.node, {
|
|
3695
|
-
x: placed.x,
|
|
3696
|
-
y: placed.y,
|
|
3697
|
-
w: placed.w,
|
|
3698
|
-
h: placed.h
|
|
3699
|
-
}, depth)
|
|
3555
|
+
lines.push(
|
|
3556
|
+
field(
|
|
3557
|
+
"estructura",
|
|
3558
|
+
`${num(result.files)} ficheros \xB7 ${num(result.folders)} carpetas \xB7 ${num(
|
|
3559
|
+
result.edges
|
|
3560
|
+
)} aristas \xB7 ${num(result.cycles.length)} ciclos`
|
|
3561
|
+
)
|
|
3700
3562
|
);
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3563
|
+
lines.push(
|
|
3564
|
+
field(
|
|
3565
|
+
"imports",
|
|
3566
|
+
`${num(result.externalImports)} externos \xB7 ${num(
|
|
3567
|
+
result.builtinImports
|
|
3568
|
+
)} builtins \xB7 ${num(result.typeOnlyImports)} type-only \xB7 ${num(
|
|
3569
|
+
result.unresolvedImports
|
|
3570
|
+
)} sin resolver`
|
|
3571
|
+
)
|
|
3572
|
+
);
|
|
3573
|
+
const migrationLanguages = Object.entries(result.migration.languages).map(([language, stats]) => `${language} ${num(stats?.rloc ?? 0)}`).join(" \xB7 ");
|
|
3574
|
+
lines.push(
|
|
3575
|
+
field(
|
|
3576
|
+
"migraci\xF3n",
|
|
3577
|
+
`${migrationLanguages || "\u2014"} \xB7 ${num(
|
|
3578
|
+
result.migration.bridgeFiles
|
|
3579
|
+
)} puentes \xB7 ${num(result.migration.ffiEdges)} FFI \xB7 deuda ${num(
|
|
3580
|
+
result.migration.migrationDebt
|
|
3581
|
+
)}`
|
|
3582
|
+
)
|
|
3583
|
+
);
|
|
3584
|
+
lines.push(field("tiempo", `${num(result.durationMs)} ms`));
|
|
3585
|
+
lines.push(
|
|
3586
|
+
field(
|
|
3587
|
+
"historial",
|
|
3588
|
+
result.git.enabled ? `${num(result.git.commits)} commits \xB7 ${num(
|
|
3589
|
+
result.git.authors
|
|
3590
|
+
)} autores${result.git.shallow ? " \xB7 shallow" : ""}` : "sin git; se omite churn"
|
|
3591
|
+
)
|
|
3592
|
+
);
|
|
3593
|
+
for (const warning of result.warnings.slice(0, 10)) {
|
|
3594
|
+
lines.push(field("aviso", warning));
|
|
3705
3595
|
}
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3596
|
+
if (result.warnings.length > 10) {
|
|
3597
|
+
lines.push(field("aviso", `y ${num(result.warnings.length - 10)} m\xE1s`));
|
|
3598
|
+
}
|
|
3599
|
+
for (const issue of result.unresolved.slice(0, 5)) {
|
|
3600
|
+
lines.push(
|
|
3601
|
+
field(
|
|
3602
|
+
"sin resolver",
|
|
3603
|
+
`${issue.file}:${issue.line} ${issue.specifier} (${issue.reason})`
|
|
3604
|
+
)
|
|
3605
|
+
);
|
|
3606
|
+
}
|
|
3607
|
+
if (result.unresolved.length > 5) {
|
|
3608
|
+
lines.push(
|
|
3609
|
+
field("sin resolver", `y ${num(result.unresolved.length - 5)} m\xE1s`)
|
|
3610
|
+
);
|
|
3709
3611
|
}
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
w: Math.max(0, rect.w - 2),
|
|
3714
|
-
h: Math.max(0, rect.h - headerHeight - 2)
|
|
3715
|
-
};
|
|
3716
|
-
const children = depth < MAX_DEPTH3 && inner.w >= 1 && inner.h >= 1 ? layoutChildren(node.children, inner, depth + 1) : [];
|
|
3717
|
-
return { node, rect, headerHeight, children };
|
|
3612
|
+
lines.push("");
|
|
3613
|
+
console.log(lines.join("\n"));
|
|
3614
|
+
return 0;
|
|
3718
3615
|
}
|
|
3719
3616
|
|
|
3720
|
-
//
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
}
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
function formatCompact(value) {
|
|
3732
|
-
if (!Number.isFinite(value)) return "\u2014";
|
|
3733
|
-
const absolute = Math.abs(value);
|
|
3734
|
-
if (absolute >= 1e6) {
|
|
3735
|
-
return `${(value / 1e6).toFixed(1).replace(".0", "").replace(".", ",")}M`;
|
|
3617
|
+
// apps/cli/src/commands/diff.ts
|
|
3618
|
+
import fs8 from "node:fs";
|
|
3619
|
+
import path6 from "node:path";
|
|
3620
|
+
|
|
3621
|
+
// packages/metrics/src/config.ts
|
|
3622
|
+
import fs7 from "node:fs";
|
|
3623
|
+
import { parse as parseYaml } from "yaml";
|
|
3624
|
+
var ConfigError = class extends Error {
|
|
3625
|
+
constructor(message) {
|
|
3626
|
+
super(message);
|
|
3627
|
+
this.name = "ConfigError";
|
|
3736
3628
|
}
|
|
3737
|
-
|
|
3738
|
-
|
|
3629
|
+
};
|
|
3630
|
+
var SEVERITIES = /* @__PURE__ */ new Set(["info", "warn", "error"]);
|
|
3631
|
+
function parseRuleConfig(input) {
|
|
3632
|
+
const root = asObject(input, "config");
|
|
3633
|
+
if (root.version !== 1) {
|
|
3634
|
+
throw new ConfigError("config.version debe ser 1");
|
|
3739
3635
|
}
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
return Object.is(rounded, -0) ? "0" : String(rounded);
|
|
3745
|
-
}
|
|
3746
|
-
function span(value) {
|
|
3747
|
-
return String(Math.max(1, Math.round(value)));
|
|
3636
|
+
const layers = parseLayers(root.layers);
|
|
3637
|
+
const layerNames = new Set(layers.map((layer) => layer.name));
|
|
3638
|
+
const rules = parseRules(root.rules, layerNames);
|
|
3639
|
+
return { version: 1, layers, rules };
|
|
3748
3640
|
}
|
|
3749
|
-
function
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3641
|
+
function loadRuleConfig(file) {
|
|
3642
|
+
let text;
|
|
3643
|
+
try {
|
|
3644
|
+
text = fs7.readFileSync(file, "utf8");
|
|
3645
|
+
} catch (error) {
|
|
3646
|
+
throw new ConfigError(
|
|
3647
|
+
`no se pudo leer ${file}: ${error.message}`
|
|
3648
|
+
);
|
|
3649
|
+
}
|
|
3650
|
+
let value;
|
|
3651
|
+
try {
|
|
3652
|
+
value = parseYaml(text);
|
|
3653
|
+
} catch (error) {
|
|
3654
|
+
throw new ConfigError(
|
|
3655
|
+
`YAML inv\xE1lido en ${file}: ${error.message}`
|
|
3656
|
+
);
|
|
3657
|
+
}
|
|
3658
|
+
try {
|
|
3659
|
+
return parseRuleConfig(value);
|
|
3660
|
+
} catch (error) {
|
|
3661
|
+
if (error instanceof ConfigError) {
|
|
3662
|
+
throw new ConfigError(`${file}: ${error.message}`);
|
|
3663
|
+
}
|
|
3664
|
+
throw error;
|
|
3665
|
+
}
|
|
3755
3666
|
}
|
|
3756
|
-
function
|
|
3757
|
-
if (
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3667
|
+
function parseLayers(value) {
|
|
3668
|
+
if (value === void 0) return [];
|
|
3669
|
+
if (!Array.isArray(value)) {
|
|
3670
|
+
throw new ConfigError("config.layers debe ser una lista");
|
|
3671
|
+
}
|
|
3672
|
+
const layers = [];
|
|
3673
|
+
const names = /* @__PURE__ */ new Set();
|
|
3674
|
+
value.forEach((raw, index) => {
|
|
3675
|
+
const layer = asObject(raw, `config.layers[${index}]`);
|
|
3676
|
+
const name = asString(layer.name, `config.layers[${index}].name`);
|
|
3677
|
+
if (names.has(name)) {
|
|
3678
|
+
throw new ConfigError(`capa duplicada: ${name}`);
|
|
3679
|
+
}
|
|
3680
|
+
names.add(name);
|
|
3681
|
+
if (layer.patterns !== void 0 && layer.paths !== void 0) {
|
|
3682
|
+
throw new ConfigError(
|
|
3683
|
+
`config.layers[${index}] no puede tener patterns y paths a la vez`
|
|
3767
3684
|
);
|
|
3768
3685
|
}
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3686
|
+
const rawPatterns = layer.patterns ?? layer.paths;
|
|
3687
|
+
if (!Array.isArray(rawPatterns) || rawPatterns.length === 0) {
|
|
3688
|
+
throw new ConfigError(
|
|
3689
|
+
`config.layers[${index}].patterns debe ser una lista no vac\xEDa`
|
|
3772
3690
|
);
|
|
3773
3691
|
}
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3692
|
+
const patterns = rawPatterns.map((rawPattern, patternIndex) => {
|
|
3693
|
+
const pattern = asString(
|
|
3694
|
+
rawPattern,
|
|
3695
|
+
`config.layers[${index}].patterns[${patternIndex}]`
|
|
3696
|
+
).replaceAll("\\", "/").replace(/^\.\//, "");
|
|
3697
|
+
if (pattern.startsWith("/") || /^[A-Za-z]:\//.test(pattern)) {
|
|
3698
|
+
throw new ConfigError(
|
|
3699
|
+
`el patr\xF3n "${pattern}" debe ser relativo al root del proyecto`
|
|
3700
|
+
);
|
|
3701
|
+
}
|
|
3702
|
+
return pattern;
|
|
3703
|
+
});
|
|
3704
|
+
layers.push({ name, patterns });
|
|
3705
|
+
});
|
|
3706
|
+
return layers;
|
|
3781
3707
|
}
|
|
3782
|
-
function
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
const
|
|
3791
|
-
const
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3708
|
+
function parseRules(value, layerNames) {
|
|
3709
|
+
if (value === void 0) return [];
|
|
3710
|
+
if (!Array.isArray(value)) {
|
|
3711
|
+
throw new ConfigError("config.rules debe ser una lista");
|
|
3712
|
+
}
|
|
3713
|
+
const rules = [];
|
|
3714
|
+
const names = /* @__PURE__ */ new Set();
|
|
3715
|
+
value.forEach((raw, index) => {
|
|
3716
|
+
const rule = asObject(raw, `config.rules[${index}]`);
|
|
3717
|
+
const name = asString(rule.name, `config.rules[${index}].name`);
|
|
3718
|
+
if (names.has(name)) {
|
|
3719
|
+
throw new ConfigError(`regla duplicada: ${name}`);
|
|
3720
|
+
}
|
|
3721
|
+
names.add(name);
|
|
3722
|
+
const type = asString(rule.type, `config.rules[${index}].type`);
|
|
3723
|
+
const severity = parseSeverity(
|
|
3724
|
+
rule.severity,
|
|
3725
|
+
`config.rules[${index}].severity`
|
|
3797
3726
|
);
|
|
3798
|
-
const
|
|
3799
|
-
|
|
3800
|
-
|
|
3727
|
+
const message = rule.message === void 0 ? void 0 : asString(rule.message, `config.rules[${index}].message`);
|
|
3728
|
+
if (type === "forbidden") {
|
|
3729
|
+
const from = asString(rule.from, `config.rules[${index}].from`);
|
|
3730
|
+
const to = asString(rule.to, `config.rules[${index}].to`);
|
|
3731
|
+
if (!layerNames.has(from)) {
|
|
3732
|
+
throw new ConfigError(
|
|
3733
|
+
`config.rules[${index}].from apunta a una capa inexistente: ${from}`
|
|
3734
|
+
);
|
|
3735
|
+
}
|
|
3736
|
+
if (!layerNames.has(to)) {
|
|
3737
|
+
throw new ConfigError(
|
|
3738
|
+
`config.rules[${index}].to apunta a una capa inexistente: ${to}`
|
|
3739
|
+
);
|
|
3740
|
+
}
|
|
3741
|
+
rules.push({ type, name, from, to, severity, message });
|
|
3742
|
+
return;
|
|
3743
|
+
}
|
|
3744
|
+
if (type === "threshold") {
|
|
3745
|
+
const metric = asString(
|
|
3746
|
+
rule.metric,
|
|
3747
|
+
`config.rules[${index}].metric`
|
|
3748
|
+
);
|
|
3749
|
+
const min = optionalNumber(rule.min, `config.rules[${index}].min`);
|
|
3750
|
+
const max = optionalNumber(rule.max, `config.rules[${index}].max`);
|
|
3751
|
+
if (min === void 0 && max === void 0) {
|
|
3752
|
+
throw new ConfigError(
|
|
3753
|
+
`config.rules[${index}] necesita min o max`
|
|
3754
|
+
);
|
|
3755
|
+
}
|
|
3756
|
+
if (min !== void 0 && max !== void 0 && min > max) {
|
|
3757
|
+
throw new ConfigError(
|
|
3758
|
+
`config.rules[${index}]: min no puede ser mayor que max`
|
|
3759
|
+
);
|
|
3760
|
+
}
|
|
3761
|
+
rules.push({ type, name, metric, min, max, severity, message });
|
|
3762
|
+
return;
|
|
3763
|
+
}
|
|
3764
|
+
if (type === "cycles") {
|
|
3765
|
+
rules.push({ type, name, severity, message });
|
|
3766
|
+
return;
|
|
3767
|
+
}
|
|
3768
|
+
throw new ConfigError(
|
|
3769
|
+
`config.rules[${index}].type desconocido: ${type}`
|
|
3801
3770
|
);
|
|
3802
|
-
|
|
3803
|
-
|
|
3771
|
+
});
|
|
3772
|
+
return rules;
|
|
3773
|
+
}
|
|
3774
|
+
function parseSeverity(value, path9) {
|
|
3775
|
+
if (value === void 0) return "error";
|
|
3776
|
+
if (typeof value !== "string" || !SEVERITIES.has(value)) {
|
|
3777
|
+
throw new ConfigError(`${path9} debe ser info, warn o error`);
|
|
3804
3778
|
}
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3779
|
+
return value;
|
|
3780
|
+
}
|
|
3781
|
+
function optionalNumber(value, path9) {
|
|
3782
|
+
if (value === void 0) return void 0;
|
|
3783
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
3784
|
+
throw new ConfigError(`${path9} debe ser un n\xFAmero`);
|
|
3785
|
+
}
|
|
3786
|
+
return value;
|
|
3787
|
+
}
|
|
3788
|
+
function asObject(value, path9) {
|
|
3789
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
3790
|
+
throw new ConfigError(`${path9} debe ser un objeto`);
|
|
3791
|
+
}
|
|
3792
|
+
return value;
|
|
3793
|
+
}
|
|
3794
|
+
function asString(value, path9) {
|
|
3795
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
3796
|
+
throw new ConfigError(`${path9} debe ser un texto no vac\xEDo`);
|
|
3797
|
+
}
|
|
3798
|
+
return value.trim();
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3801
|
+
// packages/metrics/src/rules.ts
|
|
3802
|
+
import { minimatch } from "minimatch";
|
|
3803
|
+
function emptyRuleReport() {
|
|
3804
|
+
return {
|
|
3805
|
+
configured: false,
|
|
3806
|
+
summary: {
|
|
3807
|
+
layers: 0,
|
|
3808
|
+
rules: 0,
|
|
3809
|
+
introduced: 0,
|
|
3810
|
+
existing: 0,
|
|
3811
|
+
resolved: 0,
|
|
3812
|
+
introducedErrors: 0,
|
|
3813
|
+
introducedWarnings: 0
|
|
3814
|
+
},
|
|
3815
|
+
violations: { introduced: [], existing: [], resolved: [] }
|
|
3816
|
+
};
|
|
3817
|
+
}
|
|
3818
|
+
function evaluateRules(base, head, config) {
|
|
3819
|
+
const baseViolations = evaluateInput(base, config);
|
|
3820
|
+
const headViolations = evaluateInput(head, config);
|
|
3821
|
+
const baseByKey = new Map(
|
|
3822
|
+
baseViolations.map((violation) => [violation.key, violation])
|
|
3808
3823
|
);
|
|
3809
|
-
|
|
3810
|
-
|
|
3824
|
+
const headByKey = new Map(
|
|
3825
|
+
headViolations.map((violation) => [violation.key, violation])
|
|
3811
3826
|
);
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
);
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3827
|
+
const introduced = [];
|
|
3828
|
+
const existing = [];
|
|
3829
|
+
const resolved = [];
|
|
3830
|
+
for (const [key, violation] of headByKey) {
|
|
3831
|
+
if (baseByKey.has(key)) existing.push(violation);
|
|
3832
|
+
else introduced.push(violation);
|
|
3833
|
+
}
|
|
3834
|
+
for (const [key, violation] of baseByKey) {
|
|
3835
|
+
if (!headByKey.has(key)) resolved.push(violation);
|
|
3836
|
+
}
|
|
3837
|
+
introduced.sort(compareViolations);
|
|
3838
|
+
existing.sort(compareViolations);
|
|
3839
|
+
resolved.sort(compareViolations);
|
|
3840
|
+
return {
|
|
3841
|
+
configured: true,
|
|
3842
|
+
summary: {
|
|
3843
|
+
layers: config.layers.length,
|
|
3844
|
+
rules: config.rules.length,
|
|
3845
|
+
introduced: introduced.length,
|
|
3846
|
+
existing: existing.length,
|
|
3847
|
+
resolved: resolved.length,
|
|
3848
|
+
introducedErrors: introduced.filter(
|
|
3849
|
+
(violation) => violation.severity === "error"
|
|
3850
|
+
).length,
|
|
3851
|
+
introducedWarnings: introduced.filter(
|
|
3852
|
+
(violation) => violation.severity === "warn"
|
|
3853
|
+
).length
|
|
3854
|
+
},
|
|
3855
|
+
violations: { introduced, existing, resolved }
|
|
3856
|
+
};
|
|
3857
|
+
}
|
|
3858
|
+
function evaluateInput(input, config) {
|
|
3859
|
+
const layers = layerIndex(input.files, config.layers);
|
|
3860
|
+
const violations = [];
|
|
3861
|
+
for (const rule of config.rules) {
|
|
3862
|
+
if (rule.type === "forbidden") {
|
|
3863
|
+
evaluateForbidden(rule, input, layers, violations);
|
|
3864
|
+
} else if (rule.type === "threshold") {
|
|
3865
|
+
evaluateThreshold(rule, input, violations);
|
|
3866
|
+
} else {
|
|
3867
|
+
evaluateCycles(rule, input.cycles, violations);
|
|
3825
3868
|
}
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3869
|
+
}
|
|
3870
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
3871
|
+
for (const violation of violations) byKey.set(violation.key, violation);
|
|
3872
|
+
return [...byKey.values()].sort(compareViolations);
|
|
3873
|
+
}
|
|
3874
|
+
function layerIndex(files, layers) {
|
|
3875
|
+
const index = /* @__PURE__ */ new Map();
|
|
3876
|
+
for (const file of files) {
|
|
3877
|
+
for (const layer of layers) {
|
|
3878
|
+
if (layer.patterns.some(
|
|
3879
|
+
(pattern) => minimatch(file.path, pattern, { dot: true })
|
|
3880
|
+
)) {
|
|
3881
|
+
index.set(file.id, layer.name);
|
|
3882
|
+
break;
|
|
3883
|
+
}
|
|
3830
3884
|
}
|
|
3831
3885
|
}
|
|
3832
|
-
|
|
3833
|
-
|
|
3886
|
+
return index;
|
|
3887
|
+
}
|
|
3888
|
+
function evaluateForbidden(rule, input, layers, violations) {
|
|
3889
|
+
for (const edge of input.edges) {
|
|
3890
|
+
if (layers.get(edge.fromId) !== rule.from) continue;
|
|
3891
|
+
if (layers.get(edge.toId) !== rule.to) continue;
|
|
3892
|
+
violations.push({
|
|
3893
|
+
key: `forbidden:${rule.name}:${edge.fromId}->${edge.toId}`,
|
|
3894
|
+
rule: rule.name,
|
|
3895
|
+
type: "forbidden",
|
|
3896
|
+
severity: rule.severity,
|
|
3897
|
+
message: rule.message ?? `${edge.from} no puede depender de ${edge.to}`,
|
|
3898
|
+
files: [edge.from, edge.to],
|
|
3899
|
+
edge: {
|
|
3900
|
+
fromId: edge.fromId,
|
|
3901
|
+
toId: edge.toId,
|
|
3902
|
+
from: edge.from,
|
|
3903
|
+
to: edge.to
|
|
3904
|
+
}
|
|
3905
|
+
});
|
|
3834
3906
|
}
|
|
3835
|
-
out.push("</g>");
|
|
3836
3907
|
}
|
|
3837
|
-
function
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3908
|
+
function evaluateThreshold(rule, input, violations) {
|
|
3909
|
+
for (const file of input.files) {
|
|
3910
|
+
const value = file.metrics[rule.metric];
|
|
3911
|
+
if (typeof value !== "number" || !Number.isFinite(value)) continue;
|
|
3912
|
+
const tooHigh = rule.max !== void 0 && value > rule.max;
|
|
3913
|
+
const tooLow = rule.min !== void 0 && value < rule.min;
|
|
3914
|
+
if (!tooHigh && !tooLow) continue;
|
|
3915
|
+
const limit = tooHigh && rule.max !== void 0 ? `m\xE1ximo ${rule.max}` : `m\xEDnimo ${rule.min}`;
|
|
3916
|
+
violations.push({
|
|
3917
|
+
key: `threshold:${rule.name}:${file.id}`,
|
|
3918
|
+
rule: rule.name,
|
|
3919
|
+
type: "threshold",
|
|
3920
|
+
severity: rule.severity,
|
|
3921
|
+
message: rule.message ?? `${file.path}: ${rule.metric} = ${value} (${limit})`,
|
|
3922
|
+
files: [file.path],
|
|
3923
|
+
metric: {
|
|
3924
|
+
name: rule.metric,
|
|
3925
|
+
value,
|
|
3926
|
+
...rule.min !== void 0 ? { min: rule.min } : {},
|
|
3927
|
+
...rule.max !== void 0 ? { max: rule.max } : {}
|
|
3928
|
+
}
|
|
3929
|
+
});
|
|
3850
3930
|
}
|
|
3851
|
-
out.push(`</g><g id="edges"></g></svg>`);
|
|
3852
|
-
return out.join("");
|
|
3853
3931
|
}
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3932
|
+
function evaluateCycles(rule, cycles, violations) {
|
|
3933
|
+
for (const cycle of cycles) {
|
|
3934
|
+
const paths = cycle.nodes.map((node) => node.path);
|
|
3935
|
+
violations.push({
|
|
3936
|
+
key: `cycles:${rule.name}:${cycle.nodes.map((node) => node.id).sort().join("|")}`,
|
|
3937
|
+
rule: rule.name,
|
|
3938
|
+
type: "cycles",
|
|
3939
|
+
severity: rule.severity,
|
|
3940
|
+
message: rule.message ?? cycleMessage(paths),
|
|
3941
|
+
files: paths,
|
|
3942
|
+
cycle: {
|
|
3943
|
+
size: cycle.size,
|
|
3944
|
+
nodes: cycle.nodes.map((node) => ({ ...node }))
|
|
3945
|
+
}
|
|
3946
|
+
});
|
|
3947
|
+
}
|
|
3864
3948
|
}
|
|
3865
|
-
function
|
|
3866
|
-
|
|
3949
|
+
function cycleMessage(paths) {
|
|
3950
|
+
const visible = paths.slice(0, 6);
|
|
3951
|
+
const suffix = paths.length > visible.length ? " \u2192 \u2026" : "";
|
|
3952
|
+
return `Ciclo de ${paths.length} ficheros: ${visible.join(" \u2192 ")}${suffix}`;
|
|
3867
3953
|
}
|
|
3868
|
-
function
|
|
3869
|
-
|
|
3870
|
-
const directories = [];
|
|
3871
|
-
const fileRows = model.files.map((file) => {
|
|
3872
|
-
const slash = file.path.lastIndexOf("/");
|
|
3873
|
-
const directory = slash >= 0 ? file.path.slice(0, slash) : "";
|
|
3874
|
-
let dirIndex = -1;
|
|
3875
|
-
if (directory) {
|
|
3876
|
-
const known = directoryIndex.get(directory);
|
|
3877
|
-
if (known === void 0) {
|
|
3878
|
-
dirIndex = directories.length;
|
|
3879
|
-
directoryIndex.set(directory, dirIndex);
|
|
3880
|
-
directories.push(directory);
|
|
3881
|
-
} else {
|
|
3882
|
-
dirIndex = known;
|
|
3883
|
-
}
|
|
3884
|
-
}
|
|
3885
|
-
const name = slash >= 0 ? file.path.slice(slash + 1) : file.path;
|
|
3886
|
-
return [
|
|
3887
|
-
dirIndex,
|
|
3888
|
-
name,
|
|
3889
|
-
file.section,
|
|
3890
|
-
round(file.area, 1),
|
|
3891
|
-
round(file.actualArea, 1),
|
|
3892
|
-
round(file.risk, 4),
|
|
3893
|
-
round(file.complexity, 1),
|
|
3894
|
-
round(file.churn, 1),
|
|
3895
|
-
...file.values.map((value) => value === null ? null : round(value, 2))
|
|
3896
|
-
];
|
|
3897
|
-
});
|
|
3898
|
-
return {
|
|
3899
|
-
v: 1,
|
|
3900
|
-
meta: {
|
|
3901
|
-
project: model.meta.project,
|
|
3902
|
-
commit: model.meta.commit,
|
|
3903
|
-
checksum: model.meta.checksum,
|
|
3904
|
-
api: model.meta.apiVersion,
|
|
3905
|
-
root: model.meta.rootName
|
|
3906
|
-
},
|
|
3907
|
-
area: {
|
|
3908
|
-
key: model.areaMetric,
|
|
3909
|
-
title: model.areaTitle,
|
|
3910
|
-
total: round(model.totalArea, 1)
|
|
3911
|
-
},
|
|
3912
|
-
lens: model.initialLens,
|
|
3913
|
-
metric: model.initialMetric,
|
|
3914
|
-
metrics: model.metrics.map((metric) => ({
|
|
3915
|
-
key: metric.key,
|
|
3916
|
-
title: metric.title,
|
|
3917
|
-
min: round(metric.min, 2),
|
|
3918
|
-
max: round(metric.max, 2),
|
|
3919
|
-
mode: metric.mode,
|
|
3920
|
-
dir: metric.direction,
|
|
3921
|
-
coverage: metric.coverage
|
|
3922
|
-
})),
|
|
3923
|
-
sections: model.sections.map((section) => ({
|
|
3924
|
-
key: section.key,
|
|
3925
|
-
color: section.color,
|
|
3926
|
-
files: section.files,
|
|
3927
|
-
area: round(section.area, 1)
|
|
3928
|
-
})),
|
|
3929
|
-
dirs: directories,
|
|
3930
|
-
files: fileRows,
|
|
3931
|
-
edges: model.edges.flatMap((edge) => [edge.from, edge.to]),
|
|
3932
|
-
cycles: model.cycles.flatMap((edge) => [edge.from, edge.to]),
|
|
3933
|
-
hot: model.hotspotCount,
|
|
3934
|
-
default: model.defaultSelection,
|
|
3935
|
-
fileCount: model.fileCount,
|
|
3936
|
-
folderCount: model.folderCount,
|
|
3937
|
-
totalArea: round(model.totalArea, 1)
|
|
3938
|
-
};
|
|
3954
|
+
function compareViolations(left, right) {
|
|
3955
|
+
return compareStrings4(left.key, right.key);
|
|
3939
3956
|
}
|
|
3940
|
-
function
|
|
3941
|
-
|
|
3942
|
-
{ id: "structure", label: "Estructura" },
|
|
3943
|
-
{ id: "risk", label: "Riesgo" }
|
|
3944
|
-
];
|
|
3945
|
-
if (model.metrics.length > 0) lenses.push({ id: "metric", label: "M\xE9trica" });
|
|
3946
|
-
return lenses.map(
|
|
3947
|
-
(lens) => `<button type="button" class="lens" role="tab" data-lens="${lens.id}" aria-selected="${String(
|
|
3948
|
-
model.initialLens === lens.id
|
|
3949
|
-
)}">${lens.label}</button>`
|
|
3950
|
-
).join("");
|
|
3957
|
+
function compareStrings4(left, right) {
|
|
3958
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
3951
3959
|
}
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
model.fileCount
|
|
3971
|
-
)}</b> ficheros</span>`
|
|
3972
|
-
);
|
|
3973
|
-
chips.push(
|
|
3974
|
-
`<span class="finding static"><i class="dot neutral"></i><b>${formatInt(
|
|
3975
|
-
model.edges.length
|
|
3976
|
-
)}</b> dependencias</span>`
|
|
3960
|
+
|
|
3961
|
+
// packages/metrics/src/diff.ts
|
|
3962
|
+
function diffSnapshots(base, head, options = {}) {
|
|
3963
|
+
const top = Math.max(1, Math.floor(options.top ?? 10));
|
|
3964
|
+
const baseIndex = indexSnapshot(base);
|
|
3965
|
+
const headIndex = indexSnapshot(head);
|
|
3966
|
+
const added = difference(headIndex, baseIndex);
|
|
3967
|
+
const removed = difference(baseIndex, headIndex);
|
|
3968
|
+
const changed = changedFiles(baseIndex, headIndex);
|
|
3969
|
+
const edgeDiff = diffEdges(baseIndex, headIndex);
|
|
3970
|
+
const baseCycles = cycleGroups(baseIndex);
|
|
3971
|
+
const headCycles = cycleGroups(headIndex);
|
|
3972
|
+
const introducedCycles = cycleDifference(headCycles, baseCycles);
|
|
3973
|
+
const fixedCycles = cycleDifference(baseCycles, headCycles);
|
|
3974
|
+
const hotspotMetric = chooseHotspotMetric(
|
|
3975
|
+
baseIndex,
|
|
3976
|
+
headIndex,
|
|
3977
|
+
options.hotspotMetric
|
|
3977
3978
|
);
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3979
|
+
const hotspots = hotspotMetric ? buildHotspots(baseIndex, headIndex, added, changed, hotspotMetric, top) : [];
|
|
3980
|
+
const rules = options.rules ? evaluateRules(
|
|
3981
|
+
ruleInput(baseIndex, [...baseCycles.values()].map((group) => group.component)),
|
|
3982
|
+
ruleInput(headIndex, [...headCycles.values()].map((group) => group.component)),
|
|
3983
|
+
options.rules
|
|
3984
|
+
) : emptyRuleReport();
|
|
3985
|
+
return {
|
|
3986
|
+
schemaVersion: "1.0",
|
|
3987
|
+
base: summarize(baseIndex),
|
|
3988
|
+
head: summarize(headIndex),
|
|
3989
|
+
summary: {
|
|
3990
|
+
addedFiles: added.length,
|
|
3991
|
+
removedFiles: removed.length,
|
|
3992
|
+
changedFiles: changed.length,
|
|
3993
|
+
addedEdges: edgeDiff.added.length,
|
|
3994
|
+
removedEdges: edgeDiff.removed.length,
|
|
3995
|
+
cyclicEdgesAdded: edgeDiff.cyclicAdded.length,
|
|
3996
|
+
cyclicEdgesRemoved: edgeDiff.cyclicRemoved.length,
|
|
3997
|
+
introducedCycles: introducedCycles.length,
|
|
3998
|
+
fixedCycles: fixedCycles.length,
|
|
3999
|
+
hotspots: hotspots.length
|
|
4000
|
+
},
|
|
4001
|
+
hotspotMetric,
|
|
4002
|
+
rules,
|
|
4003
|
+
files: { added, removed, changed },
|
|
4004
|
+
edges: edgeDiff,
|
|
4005
|
+
cycles: { introduced: introducedCycles, fixed: fixedCycles },
|
|
4006
|
+
hotspots
|
|
4007
|
+
};
|
|
3986
4008
|
}
|
|
3987
|
-
function
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
4009
|
+
function indexSnapshot(cc) {
|
|
4010
|
+
const paths = /* @__PURE__ */ new Map();
|
|
4011
|
+
for (const root of cc.files) {
|
|
4012
|
+
walkFiles([root], (entry) => {
|
|
4013
|
+
paths.set(entry.node.id, relativePath(root, entry.path, entry.node));
|
|
4014
|
+
});
|
|
3993
4015
|
}
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4016
|
+
const nodes = [...paths.keys()].sort(
|
|
4017
|
+
(left, right) => compareStrings5(paths.get(left) ?? left, paths.get(right) ?? right)
|
|
4018
|
+
);
|
|
4019
|
+
const metrics = cc.lenses?.metrics?.attributes ?? {};
|
|
4020
|
+
const edges = /* @__PURE__ */ new Map();
|
|
4021
|
+
for (const edge of cc.lenses?.dependency?.edges ?? []) {
|
|
4022
|
+
const key = edgeKey(edge.fromId, edge.toId);
|
|
4023
|
+
const weight = finiteNumber(edge.attributes?.weight) ?? 1;
|
|
4024
|
+
const typeOnly = finiteNumber(edge.attributes?.typeOnly) ?? 0;
|
|
4025
|
+
const existing = edges.get(key);
|
|
4026
|
+
if (existing) {
|
|
4027
|
+
existing.weight += weight;
|
|
4028
|
+
existing.typeOnly += typeOnly;
|
|
4029
|
+
existing.isCyclic = existing.isCyclic || edge.isCyclic === true;
|
|
4030
|
+
} else {
|
|
4031
|
+
edges.set(key, {
|
|
4032
|
+
fromId: edge.fromId,
|
|
4033
|
+
toId: edge.toId,
|
|
4034
|
+
weight,
|
|
4035
|
+
typeOnly,
|
|
4036
|
+
isCyclic: edge.isCyclic === true
|
|
4037
|
+
});
|
|
4003
4038
|
}
|
|
4004
4039
|
}
|
|
4005
|
-
return
|
|
4006
|
-
}
|
|
4007
|
-
function renderMetricOptions(model) {
|
|
4008
|
-
return model.metrics.map(
|
|
4009
|
-
(metric) => `<option value="${escapeAttr(metric.key)}"${metric.key === model.initialMetric ? " selected" : ""}>${escapeHtml(metric.title)}</option>`
|
|
4010
|
-
).join("");
|
|
4011
|
-
}
|
|
4012
|
-
function renderInspectorSummary(model) {
|
|
4013
|
-
const hotspots = [...model.files].sort((a, b) => b.risk - a.risk || a.path.localeCompare(b.path)).slice(0, 5);
|
|
4014
|
-
return `<div class="summary">
|
|
4015
|
-
<div class="sub">Proyecto</div>
|
|
4016
|
-
<div class="big">${formatInt(model.fileCount)}</div>
|
|
4017
|
-
<div class="sub">ficheros \xB7 ${formatInt(model.folderCount)} carpetas</div>
|
|
4018
|
-
<p>${formatInt(model.totalArea)} de ${escapeHtml(
|
|
4019
|
-
model.areaTitle.toLowerCase()
|
|
4020
|
-
)} \xB7 ${formatInt(model.metrics.length)} m\xE9tricas \xB7 ${formatInt(
|
|
4021
|
-
model.hotspotCount
|
|
4022
|
-
)} hotspots.</p>
|
|
4023
|
-
<h4>Mayor riesgo</h4>
|
|
4024
|
-
<ul class="deps">${hotspots.map(
|
|
4025
|
-
(file) => `<li data-file="${file.index}"><i class="sw" style="background:${riskColor(
|
|
4026
|
-
file.risk
|
|
4027
|
-
)}"></i><span>${escapeHtml(file.path)}</span><em>${Math.round(
|
|
4028
|
-
file.risk * 100
|
|
4029
|
-
)}</em></li>`
|
|
4030
|
-
).join("")}</ul>
|
|
4031
|
-
<p>Selecciona un bloque para ver sus m\xE9tricas y sus dependencias. Clic en la cabecera de una carpeta para enfocarla.</p>
|
|
4032
|
-
</div>`;
|
|
4040
|
+
return { cc, paths, nodes, metrics, edges };
|
|
4033
4041
|
}
|
|
4034
|
-
function
|
|
4035
|
-
|
|
4036
|
-
const
|
|
4037
|
-
|
|
4038
|
-
const checksum = model.meta.checksum ? model.meta.checksum.slice(0, 8) : "";
|
|
4039
|
-
const badge = commit || checksum || `cc.json ${model.meta.apiVersion}`;
|
|
4040
|
-
const payload = jsonForScript(serialise(model));
|
|
4041
|
-
const metricPicker = model.metrics.length > 0 ? `<label class="metric-pick" id="metric-pick"${model.initialLens === "metric" ? "" : " hidden"}><span>M\xE9trica</span><select id="metric-select">${renderMetricOptions(
|
|
4042
|
-
model
|
|
4043
|
-
)}</select></label>` : "";
|
|
4044
|
-
const crumbs = model.meta.rootName ? `${project} <span class="sep">/</span> ${escapeHtml(model.meta.rootName)}` : project;
|
|
4045
|
-
return `<!doctype html>
|
|
4046
|
-
<html lang="es">
|
|
4047
|
-
<head>
|
|
4048
|
-
<meta charset="utf-8">
|
|
4049
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
4050
|
-
<meta name="color-scheme" content="light">
|
|
4051
|
-
<meta name="generator" content="CodArch">
|
|
4052
|
-
<title>CodArch \xB7 ${title}</title>
|
|
4053
|
-
<style>${STYLES}</style>
|
|
4054
|
-
</head>
|
|
4055
|
-
<body>
|
|
4056
|
-
<div class="app">
|
|
4057
|
-
<header>
|
|
4058
|
-
<div class="brand">CodArch</div>
|
|
4059
|
-
<div class="repo">
|
|
4060
|
-
<b>${title}</b>
|
|
4061
|
-
${model.meta.rootName && model.meta.rootName !== model.meta.project ? `<span>${escapeHtml(model.meta.rootName)}</span>` : ""}
|
|
4062
|
-
<span class="commit">${escapeHtml(badge)}</span>
|
|
4063
|
-
</div>
|
|
4064
|
-
<div class="spacer"></div>
|
|
4065
|
-
<nav class="lenses" role="tablist" aria-label="Lentes">${renderLensButtons(model)}</nav>
|
|
4066
|
-
${metricPicker}
|
|
4067
|
-
<label class="search">
|
|
4068
|
-
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
4069
|
-
<circle cx="7" cy="7" r="4.5" stroke="#8d877d" stroke-width="1.4"/>
|
|
4070
|
-
<path d="M10.5 10.5 14 14" stroke="#8d877d" stroke-width="1.4"/>
|
|
4071
|
-
</svg>
|
|
4072
|
-
<input id="q" type="search" placeholder="Buscar fichero\u2026" autocomplete="off" aria-label="Buscar fichero">
|
|
4073
|
-
</label>
|
|
4074
|
-
</header>
|
|
4075
|
-
|
|
4076
|
-
<div class="findings">${renderFindings(model)}</div>
|
|
4077
|
-
|
|
4078
|
-
<main class="map-wrap">
|
|
4079
|
-
<div class="map" id="map">${renderSvg(model)}</div>
|
|
4080
|
-
<div class="tooltip" id="tooltip"></div>
|
|
4081
|
-
</main>
|
|
4082
|
-
|
|
4083
|
-
<aside id="inspector">${renderInspectorSummary(model)}</aside>
|
|
4084
|
-
|
|
4085
|
-
<footer>
|
|
4086
|
-
<span class="crumbs" id="crumbs">${crumbs}</span>
|
|
4087
|
-
<span class="hint">clic para fijar \xB7 esc para limpiar</span>
|
|
4088
|
-
<span class="legend" id="legend">${renderLegend(model)}</span>
|
|
4089
|
-
</footer>
|
|
4090
|
-
</div>
|
|
4091
|
-
<script id="codarch-data" type="application/json">${payload}</script>
|
|
4092
|
-
<script>${CLIENT}</script>
|
|
4093
|
-
</body>
|
|
4094
|
-
</html>`;
|
|
4042
|
+
function relativePath(root, fullPath, leaf) {
|
|
4043
|
+
if (fullPath === root.name) return leaf.name;
|
|
4044
|
+
const prefix = `${root.name}/`;
|
|
4045
|
+
return fullPath.startsWith(prefix) ? fullPath.slice(prefix.length) : fullPath;
|
|
4095
4046
|
}
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
}
|
|
4108
|
-
|
|
4109
|
-
|
|
4047
|
+
function summarize(index) {
|
|
4048
|
+
let folders = 0;
|
|
4049
|
+
walkTree(index.cc.files, (entry) => {
|
|
4050
|
+
if (entry.node.type === "Folder" && entry.depth > 0) folders++;
|
|
4051
|
+
});
|
|
4052
|
+
return {
|
|
4053
|
+
projectName: index.cc.meta.projectName ?? "",
|
|
4054
|
+
checksum: index.cc.meta.checksum ?? "",
|
|
4055
|
+
files: index.paths.size,
|
|
4056
|
+
folders,
|
|
4057
|
+
edges: index.edges.size
|
|
4058
|
+
};
|
|
4059
|
+
}
|
|
4060
|
+
function difference(present, absent) {
|
|
4061
|
+
const files = [];
|
|
4062
|
+
for (const [id, path9] of present.paths) {
|
|
4063
|
+
if (absent.paths.has(id)) continue;
|
|
4064
|
+
files.push({
|
|
4065
|
+
id,
|
|
4066
|
+
path: path9,
|
|
4067
|
+
metrics: sortedMetrics(present.metrics[id])
|
|
4068
|
+
});
|
|
4110
4069
|
}
|
|
4070
|
+
return files.sort(compareFiles);
|
|
4111
4071
|
}
|
|
4112
|
-
function
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4072
|
+
function changedFiles(base, head) {
|
|
4073
|
+
const changed = [];
|
|
4074
|
+
for (const [id, path9] of head.paths) {
|
|
4075
|
+
if (!base.paths.has(id)) continue;
|
|
4076
|
+
const baseValues = base.metrics[id] ?? {};
|
|
4077
|
+
const headValues = head.metrics[id] ?? {};
|
|
4078
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(baseValues), ...Object.keys(headValues)]);
|
|
4079
|
+
const changes = [];
|
|
4080
|
+
for (const metric of [...keys].sort(compareStrings5)) {
|
|
4081
|
+
const baseValue = finiteNumber(baseValues[metric]);
|
|
4082
|
+
const headValue = finiteNumber(headValues[metric]);
|
|
4083
|
+
if (baseValue === null && headValue === null) continue;
|
|
4084
|
+
if (baseValue !== null && headValue !== null && baseValue === headValue) {
|
|
4085
|
+
continue;
|
|
4086
|
+
}
|
|
4087
|
+
changes.push({
|
|
4088
|
+
metric,
|
|
4089
|
+
base: baseValue,
|
|
4090
|
+
head: headValue,
|
|
4091
|
+
delta: baseValue !== null && headValue !== null ? headValue - baseValue : null
|
|
4092
|
+
});
|
|
4093
|
+
}
|
|
4094
|
+
if (changes.length > 0) changed.push({ id, path: path9, changes });
|
|
4095
|
+
}
|
|
4096
|
+
return changed.sort(compareChangedFiles);
|
|
4116
4097
|
}
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4098
|
+
function diffEdges(base, head) {
|
|
4099
|
+
const added = [];
|
|
4100
|
+
const removed = [];
|
|
4101
|
+
for (const [key, edge] of head.edges) {
|
|
4102
|
+
if (!base.edges.has(key)) added.push(edgeChange(head, edge));
|
|
4103
|
+
}
|
|
4104
|
+
for (const [key, edge] of base.edges) {
|
|
4105
|
+
if (!head.edges.has(key)) removed.push(edgeChange(base, edge));
|
|
4106
|
+
}
|
|
4107
|
+
added.sort(compareEdgeChanges);
|
|
4108
|
+
removed.sort(compareEdgeChanges);
|
|
4109
|
+
return {
|
|
4110
|
+
added,
|
|
4111
|
+
removed,
|
|
4112
|
+
cyclicAdded: added.filter((edge) => edge.isCyclic),
|
|
4113
|
+
cyclicRemoved: removed.filter((edge) => edge.isCyclic)
|
|
4114
|
+
};
|
|
4122
4115
|
}
|
|
4123
|
-
function
|
|
4124
|
-
return
|
|
4116
|
+
function edgeChange(index, edge) {
|
|
4117
|
+
return {
|
|
4118
|
+
fromId: edge.fromId,
|
|
4119
|
+
toId: edge.toId,
|
|
4120
|
+
from: index.paths.get(edge.fromId) ?? edge.fromId,
|
|
4121
|
+
to: index.paths.get(edge.toId) ?? edge.toId,
|
|
4122
|
+
weight: edge.weight,
|
|
4123
|
+
typeOnly: edge.typeOnly,
|
|
4124
|
+
isCyclic: edge.isCyclic
|
|
4125
|
+
};
|
|
4125
4126
|
}
|
|
4126
|
-
function
|
|
4127
|
-
const
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
projectName: options.projectName,
|
|
4133
|
-
git: options.git
|
|
4134
|
-
});
|
|
4135
|
-
} catch (error) {
|
|
4136
|
-
console.error(`no se pudo analizar ${root}: ${error.message}`);
|
|
4137
|
-
return 1;
|
|
4127
|
+
function cycleGroups(index) {
|
|
4128
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
4129
|
+
const reverse = /* @__PURE__ */ new Map();
|
|
4130
|
+
for (const node of index.nodes) {
|
|
4131
|
+
adjacency.set(node, []);
|
|
4132
|
+
reverse.set(node, []);
|
|
4138
4133
|
}
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
if (error instanceof CcJsonError) {
|
|
4144
|
-
console.error(`el snapshot generado no es cc.json v\xE1lido: ${error.message}`);
|
|
4145
|
-
return 1;
|
|
4146
|
-
}
|
|
4147
|
-
throw error;
|
|
4134
|
+
for (const edge of index.edges.values()) {
|
|
4135
|
+
if (!index.paths.has(edge.fromId) || !index.paths.has(edge.toId)) continue;
|
|
4136
|
+
adjacency.get(edge.fromId)?.push(edge.toId);
|
|
4137
|
+
reverse.get(edge.toId)?.push(edge.fromId);
|
|
4148
4138
|
}
|
|
4149
|
-
const
|
|
4150
|
-
|
|
4151
|
-
);
|
|
4152
|
-
const snapshotPath = path5.join(outDirectory, "snapshot.cc.json");
|
|
4153
|
-
const mapPath = path5.join(outDirectory, "map.html");
|
|
4154
|
-
try {
|
|
4155
|
-
fs7.mkdirSync(outDirectory, { recursive: true });
|
|
4156
|
-
fs7.writeFileSync(
|
|
4157
|
-
snapshotPath,
|
|
4158
|
-
`${JSON.stringify(cc, null, 2)}
|
|
4159
|
-
`,
|
|
4160
|
-
"utf8"
|
|
4161
|
-
);
|
|
4162
|
-
} catch (error) {
|
|
4163
|
-
console.error(
|
|
4164
|
-
`no se pudo escribir ${snapshotPath}: ${error.message}`
|
|
4165
|
-
);
|
|
4166
|
-
return 1;
|
|
4139
|
+
for (const [node, neighbors] of adjacency) {
|
|
4140
|
+
adjacency.set(node, [...new Set(neighbors)].sort(compareStrings5));
|
|
4167
4141
|
}
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4142
|
+
for (const [node, neighbors] of reverse) {
|
|
4143
|
+
reverse.set(node, [...new Set(neighbors)].sort(compareStrings5));
|
|
4144
|
+
}
|
|
4145
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4146
|
+
const order = [];
|
|
4147
|
+
for (const start of index.nodes) {
|
|
4148
|
+
if (visited.has(start)) continue;
|
|
4149
|
+
visited.add(start);
|
|
4150
|
+
const stack = [
|
|
4151
|
+
{ node: start, neighbors: adjacency.get(start) ?? [], next: 0 }
|
|
4152
|
+
];
|
|
4153
|
+
while (stack.length > 0) {
|
|
4154
|
+
const frame = stack[stack.length - 1];
|
|
4155
|
+
if (frame.next < frame.neighbors.length) {
|
|
4156
|
+
const next = frame.neighbors[frame.next];
|
|
4157
|
+
frame.next++;
|
|
4158
|
+
if (!visited.has(next)) {
|
|
4159
|
+
visited.add(next);
|
|
4160
|
+
stack.push({
|
|
4161
|
+
node: next,
|
|
4162
|
+
neighbors: adjacency.get(next) ?? [],
|
|
4163
|
+
next: 0
|
|
4164
|
+
});
|
|
4165
|
+
}
|
|
4166
|
+
} else {
|
|
4167
|
+
order.push(frame.node);
|
|
4168
|
+
stack.pop();
|
|
4169
|
+
}
|
|
4182
4170
|
}
|
|
4183
|
-
} else {
|
|
4184
|
-
mapError = "no hay ficheros TypeScript/JavaScript que dibujar";
|
|
4185
4171
|
}
|
|
4186
|
-
const
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4172
|
+
const assigned = /* @__PURE__ */ new Set();
|
|
4173
|
+
const groups = /* @__PURE__ */ new Map();
|
|
4174
|
+
for (let position = order.length - 1; position >= 0; position--) {
|
|
4175
|
+
const start = order[position];
|
|
4176
|
+
if (assigned.has(start)) continue;
|
|
4177
|
+
assigned.add(start);
|
|
4178
|
+
const component = [];
|
|
4179
|
+
const stack = [start];
|
|
4180
|
+
while (stack.length > 0) {
|
|
4181
|
+
const node = stack.pop();
|
|
4182
|
+
component.push(node);
|
|
4183
|
+
for (const next of reverse.get(node) ?? []) {
|
|
4184
|
+
if (assigned.has(next)) continue;
|
|
4185
|
+
assigned.add(next);
|
|
4186
|
+
stack.push(next);
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
const hasSelfLoop = component.some(
|
|
4190
|
+
(node) => (adjacency.get(node) ?? []).includes(node)
|
|
4191
|
+
);
|
|
4192
|
+
if (component.length < 2 && !hasSelfLoop) continue;
|
|
4193
|
+
const nodes = component.map((id) => ({
|
|
4194
|
+
id,
|
|
4195
|
+
path: index.paths.get(id) ?? id
|
|
4196
|
+
})).sort((left, right) => {
|
|
4197
|
+
return compareStrings5(left.path, right.path) || compareStrings5(left.id, right.id);
|
|
4198
|
+
});
|
|
4199
|
+
const key = [...component].sort(compareStrings5).join("\0");
|
|
4200
|
+
groups.set(key, { key, component: { size: nodes.length, nodes } });
|
|
4200
4201
|
}
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
);
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
)}`
|
|
4228
|
-
)
|
|
4229
|
-
);
|
|
4230
|
-
lines.push(field("tiempo", `${num(result.durationMs)} ms`));
|
|
4231
|
-
lines.push(
|
|
4232
|
-
field(
|
|
4233
|
-
"historial",
|
|
4234
|
-
result.git.enabled ? `${num(result.git.commits)} commits \xB7 ${num(
|
|
4235
|
-
result.git.authors
|
|
4236
|
-
)} autores${result.git.shallow ? " \xB7 shallow" : ""}` : "sin git; se omite churn"
|
|
4237
|
-
)
|
|
4238
|
-
);
|
|
4239
|
-
for (const warning of result.warnings.slice(0, 10)) {
|
|
4240
|
-
lines.push(field("aviso", warning));
|
|
4202
|
+
return groups;
|
|
4203
|
+
}
|
|
4204
|
+
function cycleDifference(present, absent) {
|
|
4205
|
+
const cycles = [];
|
|
4206
|
+
for (const [key, group] of present) {
|
|
4207
|
+
if (!absent.has(key)) cycles.push(group.component);
|
|
4208
|
+
}
|
|
4209
|
+
return cycles.sort(compareCycles);
|
|
4210
|
+
}
|
|
4211
|
+
function ruleInput(index, cycles) {
|
|
4212
|
+
const files = index.nodes.map((id) => ({
|
|
4213
|
+
id,
|
|
4214
|
+
path: index.paths.get(id) ?? id,
|
|
4215
|
+
metrics: sortedMetrics(index.metrics[id])
|
|
4216
|
+
}));
|
|
4217
|
+
const edges = [];
|
|
4218
|
+
for (const edge of index.edges.values()) {
|
|
4219
|
+
if (!index.paths.has(edge.fromId) || !index.paths.has(edge.toId)) {
|
|
4220
|
+
continue;
|
|
4221
|
+
}
|
|
4222
|
+
edges.push({
|
|
4223
|
+
fromId: edge.fromId,
|
|
4224
|
+
toId: edge.toId,
|
|
4225
|
+
from: index.paths.get(edge.fromId) ?? edge.fromId,
|
|
4226
|
+
to: index.paths.get(edge.toId) ?? edge.toId
|
|
4227
|
+
});
|
|
4241
4228
|
}
|
|
4242
|
-
|
|
4243
|
-
|
|
4229
|
+
return { files, edges, cycles };
|
|
4230
|
+
}
|
|
4231
|
+
function chooseHotspotMetric(base, head, requested) {
|
|
4232
|
+
if (requested) return requested;
|
|
4233
|
+
for (const metric of ["maxComplexity", "rloc"]) {
|
|
4234
|
+
if (hasMetric(base, metric) || hasMetric(head, metric)) return metric;
|
|
4244
4235
|
}
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
)
|
|
4251
|
-
);
|
|
4236
|
+
return null;
|
|
4237
|
+
}
|
|
4238
|
+
function hasMetric(index, metric) {
|
|
4239
|
+
for (const values of Object.values(index.metrics)) {
|
|
4240
|
+
if (finiteNumber(values[metric]) !== null) return true;
|
|
4252
4241
|
}
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4242
|
+
return false;
|
|
4243
|
+
}
|
|
4244
|
+
function buildHotspots(base, head, added, changed, metric, top) {
|
|
4245
|
+
const addedIds = new Set(added.map((file) => file.id));
|
|
4246
|
+
const candidates = [...added, ...changed];
|
|
4247
|
+
const hotspots = [];
|
|
4248
|
+
for (const file of candidates) {
|
|
4249
|
+
const baseValue = finiteNumber(base.metrics[file.id]?.[metric]);
|
|
4250
|
+
const headValue = finiteNumber(head.metrics[file.id]?.[metric]);
|
|
4251
|
+
if (headValue === null) continue;
|
|
4252
|
+
const before = baseValue ?? 0;
|
|
4253
|
+
const delta = headValue - before;
|
|
4254
|
+
if (delta <= 0) continue;
|
|
4255
|
+
hotspots.push({
|
|
4256
|
+
id: file.id,
|
|
4257
|
+
path: file.path,
|
|
4258
|
+
metric,
|
|
4259
|
+
base: before,
|
|
4260
|
+
head: headValue,
|
|
4261
|
+
delta,
|
|
4262
|
+
change: addedIds.has(file.id) ? "added" : "changed"
|
|
4263
|
+
});
|
|
4257
4264
|
}
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4265
|
+
return hotspots.sort(
|
|
4266
|
+
(left, right) => right.delta - left.delta || right.head - left.head || compareStrings5(left.path, right.path)
|
|
4267
|
+
).slice(0, top);
|
|
4268
|
+
}
|
|
4269
|
+
function sortedMetrics(values) {
|
|
4270
|
+
const sorted = {};
|
|
4271
|
+
if (!values) return sorted;
|
|
4272
|
+
for (const key of Object.keys(values).sort(compareStrings5)) {
|
|
4273
|
+
const value = finiteNumber(values[key]);
|
|
4274
|
+
if (value !== null) sorted[key] = value;
|
|
4275
|
+
}
|
|
4276
|
+
return sorted;
|
|
4277
|
+
}
|
|
4278
|
+
function finiteNumber(value) {
|
|
4279
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
4280
|
+
}
|
|
4281
|
+
function edgeKey(fromId, toId) {
|
|
4282
|
+
return `${fromId}\0${toId}`;
|
|
4283
|
+
}
|
|
4284
|
+
function compareFiles(left, right) {
|
|
4285
|
+
return compareStrings5(left.path, right.path) || compareStrings5(left.id, right.id);
|
|
4286
|
+
}
|
|
4287
|
+
function compareChangedFiles(left, right) {
|
|
4288
|
+
return compareStrings5(left.path, right.path) || compareStrings5(left.id, right.id);
|
|
4289
|
+
}
|
|
4290
|
+
function compareEdgeChanges(left, right) {
|
|
4291
|
+
return compareStrings5(left.from, right.from) || compareStrings5(left.to, right.to) || compareStrings5(left.fromId, right.fromId) || compareStrings5(left.toId, right.toId);
|
|
4292
|
+
}
|
|
4293
|
+
function compareCycles(left, right) {
|
|
4294
|
+
return compareStrings5(
|
|
4295
|
+
left.nodes[0]?.path ?? "",
|
|
4296
|
+
right.nodes[0]?.path ?? ""
|
|
4297
|
+
) || left.size - right.size;
|
|
4298
|
+
}
|
|
4299
|
+
function compareStrings5(left, right) {
|
|
4300
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
4261
4301
|
}
|
|
4262
4302
|
|
|
4263
4303
|
// apps/cli/src/commands/diff.ts
|
|
4264
|
-
import fs8 from "node:fs";
|
|
4265
|
-
import path6 from "node:path";
|
|
4266
4304
|
var numberFormat2 = new Intl.NumberFormat("es-ES");
|
|
4267
4305
|
function num2(value) {
|
|
4268
4306
|
return numberFormat2.format(value);
|
|
@@ -4504,7 +4542,7 @@ function runInspect(options) {
|
|
|
4504
4542
|
const metric = options.metric ?? defaultMetric(stats);
|
|
4505
4543
|
const top = metric ? topByMetric(cc, metric, options.top) : [];
|
|
4506
4544
|
const rootPrefix = cc.files.length === 1 ? `${cc.files[0]?.name}/` : "";
|
|
4507
|
-
const displayPath = (
|
|
4545
|
+
const displayPath = (path9) => rootPrefix && path9.startsWith(rootPrefix) ? path9.slice(rootPrefix.length) : path9;
|
|
4508
4546
|
if (options.metric && !stats.metricCoverage.some((entry) => entry.name === options.metric)) {
|
|
4509
4547
|
console.error(`la m\xE9trica "${options.metric}" no existe en este fichero`);
|
|
4510
4548
|
return 1;
|
|
@@ -4702,8 +4740,440 @@ function runRender(options) {
|
|
|
4702
4740
|
return 0;
|
|
4703
4741
|
}
|
|
4704
4742
|
|
|
4743
|
+
// apps/cli/src/commands/serve.ts
|
|
4744
|
+
import fs10 from "node:fs";
|
|
4745
|
+
import http from "node:http";
|
|
4746
|
+
import path8 from "node:path";
|
|
4747
|
+
import { fileURLToPath } from "node:url";
|
|
4748
|
+
var MIME = {
|
|
4749
|
+
".html": "text/html; charset=utf-8",
|
|
4750
|
+
".js": "text/javascript; charset=utf-8",
|
|
4751
|
+
".css": "text/css; charset=utf-8",
|
|
4752
|
+
".json": "application/json; charset=utf-8",
|
|
4753
|
+
".svg": "image/svg+xml",
|
|
4754
|
+
".png": "image/png",
|
|
4755
|
+
".ico": "image/x-icon",
|
|
4756
|
+
".woff": "font/woff",
|
|
4757
|
+
".woff2": "font/woff2",
|
|
4758
|
+
".wasm": "application/wasm"
|
|
4759
|
+
};
|
|
4760
|
+
var IGNORED_WATCH = /(^|\/)(node_modules|\.git|\.codarch|dist|coverage|\.next|out|target)(\/|$)/;
|
|
4761
|
+
var IGNORED_BROWSE = /* @__PURE__ */ new Set([
|
|
4762
|
+
".git",
|
|
4763
|
+
"node_modules",
|
|
4764
|
+
".codarch",
|
|
4765
|
+
"dist",
|
|
4766
|
+
"coverage",
|
|
4767
|
+
".next",
|
|
4768
|
+
"out",
|
|
4769
|
+
"target"
|
|
4770
|
+
]);
|
|
4771
|
+
function runServe(options) {
|
|
4772
|
+
const root = path8.resolve(options.root ?? ".");
|
|
4773
|
+
const webRoot = options.web === false ? null : findWebRoot();
|
|
4774
|
+
if (options.web !== false && !webRoot) {
|
|
4775
|
+
console.error(
|
|
4776
|
+
"no se encontr\xF3 el build web. Ejecuta `npm run web:build` o usa el paquete publicado."
|
|
4777
|
+
);
|
|
4778
|
+
return 1;
|
|
4779
|
+
}
|
|
4780
|
+
let state = analyzeState(root, options.watch);
|
|
4781
|
+
let watcher = null;
|
|
4782
|
+
let timer;
|
|
4783
|
+
const clients = /* @__PURE__ */ new Set();
|
|
4784
|
+
const startWatcher = () => {
|
|
4785
|
+
if (watcher) return null;
|
|
4786
|
+
try {
|
|
4787
|
+
watcher = fs10.watch(state.root, { recursive: true }, (_event, filename) => {
|
|
4788
|
+
if (!filename) return;
|
|
4789
|
+
const relative = filename.toString().replace(/\\/g, "/");
|
|
4790
|
+
if (IGNORED_WATCH.test(relative)) return;
|
|
4791
|
+
scheduleRefresh();
|
|
4792
|
+
});
|
|
4793
|
+
return null;
|
|
4794
|
+
} catch (error) {
|
|
4795
|
+
watcher = null;
|
|
4796
|
+
return error.message;
|
|
4797
|
+
}
|
|
4798
|
+
};
|
|
4799
|
+
const stopWatcher = () => {
|
|
4800
|
+
if (timer) {
|
|
4801
|
+
clearTimeout(timer);
|
|
4802
|
+
timer = void 0;
|
|
4803
|
+
}
|
|
4804
|
+
watcher?.close();
|
|
4805
|
+
watcher = null;
|
|
4806
|
+
};
|
|
4807
|
+
const closeClients = () => {
|
|
4808
|
+
for (const client of clients) client.end();
|
|
4809
|
+
clients.clear();
|
|
4810
|
+
};
|
|
4811
|
+
const refresh = () => {
|
|
4812
|
+
if (!state.watch) return;
|
|
4813
|
+
try {
|
|
4814
|
+
const next = analyzeState(state.root, state.watch);
|
|
4815
|
+
state = next;
|
|
4816
|
+
broadcast(clients, "update", {
|
|
4817
|
+
checksum: state.checksum,
|
|
4818
|
+
files: state.files
|
|
4819
|
+
});
|
|
4820
|
+
console.log(
|
|
4821
|
+
` \u21BB ${state.files} ficheros \xB7 checksum ${state.checksum.slice(0, 8)}`
|
|
4822
|
+
);
|
|
4823
|
+
} catch (error) {
|
|
4824
|
+
const message = error.message;
|
|
4825
|
+
console.error(` no se pudo reanalizar: ${message}`);
|
|
4826
|
+
broadcast(clients, "error", { message });
|
|
4827
|
+
}
|
|
4828
|
+
};
|
|
4829
|
+
const scheduleRefresh = () => {
|
|
4830
|
+
if (timer) clearTimeout(timer);
|
|
4831
|
+
timer = setTimeout(() => {
|
|
4832
|
+
timer = void 0;
|
|
4833
|
+
refresh();
|
|
4834
|
+
}, 350);
|
|
4835
|
+
};
|
|
4836
|
+
const setWatch = (enabled) => {
|
|
4837
|
+
if (enabled === state.watch) return;
|
|
4838
|
+
if (enabled) {
|
|
4839
|
+
const error = startWatcher();
|
|
4840
|
+
if (error) throw new Error(`watch no disponible: ${error}`);
|
|
4841
|
+
state = { ...state, watch: true };
|
|
4842
|
+
} else {
|
|
4843
|
+
stopWatcher();
|
|
4844
|
+
closeClients();
|
|
4845
|
+
state = { ...state, watch: false };
|
|
4846
|
+
}
|
|
4847
|
+
broadcast(clients, "watch", { watch: state.watch });
|
|
4848
|
+
};
|
|
4849
|
+
const switchRoot = (nextRoot) => {
|
|
4850
|
+
const resolved = path8.resolve(nextRoot);
|
|
4851
|
+
const stat = fs10.statSync(resolved);
|
|
4852
|
+
if (!stat.isDirectory()) throw new Error("la ruta no es un directorio");
|
|
4853
|
+
const next = analyzeState(resolved, state.watch);
|
|
4854
|
+
stopWatcher();
|
|
4855
|
+
state = next;
|
|
4856
|
+
if (state.watch) {
|
|
4857
|
+
const error = startWatcher();
|
|
4858
|
+
if (error) {
|
|
4859
|
+
state = { ...state, watch: false };
|
|
4860
|
+
console.error(`watch no disponible en ${resolved}: ${error}`);
|
|
4861
|
+
}
|
|
4862
|
+
}
|
|
4863
|
+
broadcast(clients, "update", {
|
|
4864
|
+
checksum: state.checksum,
|
|
4865
|
+
files: state.files
|
|
4866
|
+
});
|
|
4867
|
+
console.log(
|
|
4868
|
+
` \u2192 ${state.root} \xB7 ${state.files} ficheros \xB7 ${state.watch ? "live" : "servido"}`
|
|
4869
|
+
);
|
|
4870
|
+
return state;
|
|
4871
|
+
};
|
|
4872
|
+
const meta = () => ({
|
|
4873
|
+
root: path8.basename(state.root),
|
|
4874
|
+
rootPath: state.root,
|
|
4875
|
+
watch: state.watch,
|
|
4876
|
+
files: state.files,
|
|
4877
|
+
checksum: state.checksum,
|
|
4878
|
+
warnings: state.warnings,
|
|
4879
|
+
updatedAt: state.updatedAt
|
|
4880
|
+
});
|
|
4881
|
+
const server = http.createServer(async (request, response) => {
|
|
4882
|
+
try {
|
|
4883
|
+
const host = request.headers.host ?? `${options.host}:${options.port}`;
|
|
4884
|
+
const url = new URL(request.url ?? "/", `http://${host}`);
|
|
4885
|
+
const pathname = url.pathname;
|
|
4886
|
+
if (request.method === "POST") {
|
|
4887
|
+
if (!originAllowed(request, options)) {
|
|
4888
|
+
sendError(response, 403, "origen no permitido");
|
|
4889
|
+
return;
|
|
4890
|
+
}
|
|
4891
|
+
if (pathname === "/api/root") {
|
|
4892
|
+
const body = await readBody(request);
|
|
4893
|
+
const nextRoot = typeof body === "object" && body !== null && typeof body.path === "string" ? body.path : "";
|
|
4894
|
+
if (!nextRoot.trim()) {
|
|
4895
|
+
sendError(response, 400, "falta la ruta de la carpeta");
|
|
4896
|
+
return;
|
|
4897
|
+
}
|
|
4898
|
+
const next = switchRoot(nextRoot);
|
|
4899
|
+
sendJson(response, {
|
|
4900
|
+
...meta(),
|
|
4901
|
+
rootPath: next.root,
|
|
4902
|
+
watch: next.watch
|
|
4903
|
+
});
|
|
4904
|
+
return;
|
|
4905
|
+
}
|
|
4906
|
+
if (pathname === "/api/watch") {
|
|
4907
|
+
const body = await readBody(request);
|
|
4908
|
+
const enabled = typeof body === "object" && body !== null && body.enabled === true;
|
|
4909
|
+
try {
|
|
4910
|
+
setWatch(enabled);
|
|
4911
|
+
} catch (error) {
|
|
4912
|
+
sendError(response, 400, error.message);
|
|
4913
|
+
return;
|
|
4914
|
+
}
|
|
4915
|
+
sendJson(response, meta());
|
|
4916
|
+
return;
|
|
4917
|
+
}
|
|
4918
|
+
sendError(response, 404, "endpoint desconocido");
|
|
4919
|
+
return;
|
|
4920
|
+
}
|
|
4921
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
4922
|
+
response.writeHead(405, { Allow: "GET, HEAD, POST" });
|
|
4923
|
+
response.end();
|
|
4924
|
+
return;
|
|
4925
|
+
}
|
|
4926
|
+
if (pathname === "/api/browse") {
|
|
4927
|
+
try {
|
|
4928
|
+
sendJson(response, browseDirectories(state.root, url.searchParams.get("path")));
|
|
4929
|
+
} catch (error) {
|
|
4930
|
+
sendError(response, 400, error.message);
|
|
4931
|
+
}
|
|
4932
|
+
return;
|
|
4933
|
+
}
|
|
4934
|
+
if (pathname === "/data/snapshot.cc.json") {
|
|
4935
|
+
sendJson(response, state.snapshot);
|
|
4936
|
+
return;
|
|
4937
|
+
}
|
|
4938
|
+
if (pathname === "/data/meta.json") {
|
|
4939
|
+
sendJson(response, meta());
|
|
4940
|
+
return;
|
|
4941
|
+
}
|
|
4942
|
+
if (pathname === "/data/report.json") {
|
|
4943
|
+
const report = readReport(state.root);
|
|
4944
|
+
if (report === null) {
|
|
4945
|
+
sendError(response, 404, "no hay .codarch/report.json");
|
|
4946
|
+
return;
|
|
4947
|
+
}
|
|
4948
|
+
sendJson(response, report);
|
|
4949
|
+
return;
|
|
4950
|
+
}
|
|
4951
|
+
if (pathname === "/events") {
|
|
4952
|
+
if (!state.watch) {
|
|
4953
|
+
response.writeHead(204);
|
|
4954
|
+
response.end();
|
|
4955
|
+
return;
|
|
4956
|
+
}
|
|
4957
|
+
response.writeHead(200, {
|
|
4958
|
+
"Content-Type": "text/event-stream",
|
|
4959
|
+
"Cache-Control": "no-store",
|
|
4960
|
+
Connection: "keep-alive",
|
|
4961
|
+
"X-Accel-Buffering": "no"
|
|
4962
|
+
});
|
|
4963
|
+
response.write(
|
|
4964
|
+
`event: ready
|
|
4965
|
+
data: ${JSON.stringify({
|
|
4966
|
+
checksum: state.checksum,
|
|
4967
|
+
files: state.files
|
|
4968
|
+
})}
|
|
4969
|
+
|
|
4970
|
+
`
|
|
4971
|
+
);
|
|
4972
|
+
clients.add(response);
|
|
4973
|
+
request.on("close", () => clients.delete(response));
|
|
4974
|
+
return;
|
|
4975
|
+
}
|
|
4976
|
+
if (!webRoot) {
|
|
4977
|
+
sendError(response, 404, "servidor en modo API: la web la sirve otro proceso");
|
|
4978
|
+
return;
|
|
4979
|
+
}
|
|
4980
|
+
serveStatic(webRoot, pathname, request.method === "HEAD", response);
|
|
4981
|
+
} catch (error) {
|
|
4982
|
+
if (!response.headersSent) {
|
|
4983
|
+
sendError(response, 500, error.message);
|
|
4984
|
+
} else {
|
|
4985
|
+
response.end();
|
|
4986
|
+
}
|
|
4987
|
+
}
|
|
4988
|
+
});
|
|
4989
|
+
if (state.watch) {
|
|
4990
|
+
const error = startWatcher();
|
|
4991
|
+
if (error) {
|
|
4992
|
+
state = { ...state, watch: false };
|
|
4993
|
+
console.error(`watch no disponible; se sirve el an\xE1lisis inicial: ${error}`);
|
|
4994
|
+
}
|
|
4995
|
+
}
|
|
4996
|
+
server.on("error", (error) => {
|
|
4997
|
+
console.error(`no se pudo escuchar en ${options.host}:${options.port}: ${error.message}`);
|
|
4998
|
+
process.exitCode = 1;
|
|
4999
|
+
});
|
|
5000
|
+
server.listen(options.port, options.host, () => {
|
|
5001
|
+
const url = `http://${options.host}:${options.port}`;
|
|
5002
|
+
const lines = [
|
|
5003
|
+
"",
|
|
5004
|
+
" CodArch \xB7 serve",
|
|
5005
|
+
"",
|
|
5006
|
+
` ra\xEDz ${state.root}`,
|
|
5007
|
+
` web ${webRoot ?? "modo API (la sirve Vite)"}`,
|
|
5008
|
+
` URL ${url}`,
|
|
5009
|
+
` snapshot ${state.files} ficheros \xB7 checksum ${state.checksum.slice(0, 8)}`,
|
|
5010
|
+
` modo ${state.watch ? "watch activo (SSE)" : "solo an\xE1lisis inicial"}`,
|
|
5011
|
+
""
|
|
5012
|
+
];
|
|
5013
|
+
console.log(lines.join("\n"));
|
|
5014
|
+
});
|
|
5015
|
+
const shutdown = () => {
|
|
5016
|
+
stopWatcher();
|
|
5017
|
+
closeClients();
|
|
5018
|
+
server.close(() => process.exit(0));
|
|
5019
|
+
};
|
|
5020
|
+
process.on("SIGINT", shutdown);
|
|
5021
|
+
process.on("SIGTERM", shutdown);
|
|
5022
|
+
return 0;
|
|
5023
|
+
}
|
|
5024
|
+
function analyzeState(root, watch) {
|
|
5025
|
+
const result = analyzeRepository(root, { git: true });
|
|
5026
|
+
return {
|
|
5027
|
+
root,
|
|
5028
|
+
snapshot: result.snapshot,
|
|
5029
|
+
files: result.files,
|
|
5030
|
+
checksum: result.snapshot.meta.checksum ?? "",
|
|
5031
|
+
warnings: result.warnings.slice(0, 5),
|
|
5032
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5033
|
+
watch
|
|
5034
|
+
};
|
|
5035
|
+
}
|
|
5036
|
+
function browseDirectories(root, requested) {
|
|
5037
|
+
const target = requested?.trim() ? path8.resolve(requested) : root;
|
|
5038
|
+
const stat = fs10.statSync(target);
|
|
5039
|
+
if (!stat.isDirectory()) throw new Error("la ruta no es un directorio");
|
|
5040
|
+
const entries = fs10.readdirSync(target, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !IGNORED_BROWSE.has(entry.name)).sort((left, right) => left.name.localeCompare(right.name)).slice(0, 500).map((entry) => ({
|
|
5041
|
+
name: entry.name,
|
|
5042
|
+
path: path8.join(target, entry.name),
|
|
5043
|
+
kind: "directory"
|
|
5044
|
+
}));
|
|
5045
|
+
const parent = path8.dirname(target);
|
|
5046
|
+
return {
|
|
5047
|
+
path: target,
|
|
5048
|
+
parent: parent === target ? null : parent,
|
|
5049
|
+
entries
|
|
5050
|
+
};
|
|
5051
|
+
}
|
|
5052
|
+
async function readBody(request) {
|
|
5053
|
+
const chunks = [];
|
|
5054
|
+
let size = 0;
|
|
5055
|
+
for await (const chunk of request) {
|
|
5056
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
5057
|
+
size += buffer.length;
|
|
5058
|
+
if (size > 64 * 1024) throw new Error("cuerpo demasiado grande");
|
|
5059
|
+
chunks.push(buffer);
|
|
5060
|
+
}
|
|
5061
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
5062
|
+
return text ? JSON.parse(text) : {};
|
|
5063
|
+
}
|
|
5064
|
+
function originAllowed(request, options) {
|
|
5065
|
+
const origin = request.headers.origin;
|
|
5066
|
+
if (!origin) return true;
|
|
5067
|
+
try {
|
|
5068
|
+
const parsed = new URL(origin);
|
|
5069
|
+
if (parsed.protocol !== "http:") return false;
|
|
5070
|
+
if (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1") {
|
|
5071
|
+
return true;
|
|
5072
|
+
}
|
|
5073
|
+
return origin === `http://${options.host}:${options.port}`;
|
|
5074
|
+
} catch {
|
|
5075
|
+
return false;
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
function findWebRoot() {
|
|
5079
|
+
const candidates = [
|
|
5080
|
+
fileURLToPath(new URL("./web", import.meta.url)),
|
|
5081
|
+
path8.resolve(process.cwd(), "apps", "web", "dist"),
|
|
5082
|
+
path8.resolve(
|
|
5083
|
+
path8.dirname(fileURLToPath(import.meta.url)),
|
|
5084
|
+
"..",
|
|
5085
|
+
"..",
|
|
5086
|
+
"..",
|
|
5087
|
+
"web",
|
|
5088
|
+
"dist"
|
|
5089
|
+
)
|
|
5090
|
+
];
|
|
5091
|
+
for (const candidate of candidates) {
|
|
5092
|
+
if (fs10.existsSync(path8.join(candidate, "index.html"))) return candidate;
|
|
5093
|
+
}
|
|
5094
|
+
return null;
|
|
5095
|
+
}
|
|
5096
|
+
function readReport(root) {
|
|
5097
|
+
const file = path8.join(root, ".codarch", "report.json");
|
|
5098
|
+
try {
|
|
5099
|
+
return JSON.parse(fs10.readFileSync(file, "utf8"));
|
|
5100
|
+
} catch (error) {
|
|
5101
|
+
if (error.code !== "ENOENT") {
|
|
5102
|
+
console.error(`no se pudo leer ${file}: ${error.message}`);
|
|
5103
|
+
}
|
|
5104
|
+
return null;
|
|
5105
|
+
}
|
|
5106
|
+
}
|
|
5107
|
+
function serveStatic(webRoot, pathname, headOnly, response) {
|
|
5108
|
+
let decoded;
|
|
5109
|
+
try {
|
|
5110
|
+
decoded = decodeURIComponent(pathname);
|
|
5111
|
+
} catch {
|
|
5112
|
+
response.writeHead(400);
|
|
5113
|
+
response.end();
|
|
5114
|
+
return;
|
|
5115
|
+
}
|
|
5116
|
+
const relative = decoded === "/" ? "index.html" : decoded.replace(/^\/+/, "");
|
|
5117
|
+
let file = path8.resolve(webRoot, relative);
|
|
5118
|
+
if (!isWithin2(webRoot, file) || !fs10.existsSync(file) || !fs10.statSync(file).isFile()) {
|
|
5119
|
+
file = path8.join(webRoot, "index.html");
|
|
5120
|
+
}
|
|
5121
|
+
try {
|
|
5122
|
+
const body = fs10.readFileSync(file);
|
|
5123
|
+
response.writeHead(200, {
|
|
5124
|
+
"Content-Type": MIME[path8.extname(file).toLowerCase()] ?? "application/octet-stream",
|
|
5125
|
+
"Content-Length": body.length,
|
|
5126
|
+
"Cache-Control": file.includes(`${path8.sep}assets${path8.sep}`) ? "public, max-age=31536000, immutable" : "no-store",
|
|
5127
|
+
"X-Content-Type-Options": "nosniff"
|
|
5128
|
+
});
|
|
5129
|
+
if (headOnly) response.end();
|
|
5130
|
+
else response.end(body);
|
|
5131
|
+
} catch (error) {
|
|
5132
|
+
response.writeHead(500);
|
|
5133
|
+
response.end(error.message);
|
|
5134
|
+
}
|
|
5135
|
+
}
|
|
5136
|
+
function isWithin2(root, candidate) {
|
|
5137
|
+
const relative = path8.relative(root, candidate);
|
|
5138
|
+
return relative === "" || !relative.startsWith("..") && !path8.isAbsolute(relative);
|
|
5139
|
+
}
|
|
5140
|
+
function sendJson(response, value) {
|
|
5141
|
+
const body = `${JSON.stringify(value)}
|
|
5142
|
+
`;
|
|
5143
|
+
response.writeHead(200, {
|
|
5144
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
5145
|
+
"Content-Length": Buffer.byteLength(body),
|
|
5146
|
+
"Cache-Control": "no-store",
|
|
5147
|
+
"X-Content-Type-Options": "nosniff"
|
|
5148
|
+
});
|
|
5149
|
+
response.end(body);
|
|
5150
|
+
}
|
|
5151
|
+
function sendError(response, status, message) {
|
|
5152
|
+
const body = `${JSON.stringify({ error: message })}
|
|
5153
|
+
`;
|
|
5154
|
+
response.writeHead(status, {
|
|
5155
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
5156
|
+
"Content-Length": Buffer.byteLength(body),
|
|
5157
|
+
"Cache-Control": "no-store"
|
|
5158
|
+
});
|
|
5159
|
+
response.end(body);
|
|
5160
|
+
}
|
|
5161
|
+
function broadcast(clients, event, data) {
|
|
5162
|
+
const payload = `event: ${event}
|
|
5163
|
+
data: ${JSON.stringify(data)}
|
|
5164
|
+
|
|
5165
|
+
`;
|
|
5166
|
+
for (const client of clients) {
|
|
5167
|
+
try {
|
|
5168
|
+
client.write(payload);
|
|
5169
|
+
} catch {
|
|
5170
|
+
clients.delete(client);
|
|
5171
|
+
}
|
|
5172
|
+
}
|
|
5173
|
+
}
|
|
5174
|
+
|
|
4705
5175
|
// apps/cli/src/index.ts
|
|
4706
|
-
var VERSION = true ? "0.1.
|
|
5176
|
+
var VERSION = true ? "0.1.3" : "0.0.0";
|
|
4707
5177
|
var USAGE = `
|
|
4708
5178
|
CodArch \xB7 mapa vivo de la arquitectura de tu software
|
|
4709
5179
|
|
|
@@ -4712,6 +5182,7 @@ Uso:
|
|
|
4712
5182
|
codarch inspect <fichero.cc.json[.gz]> [opciones]
|
|
4713
5183
|
codarch render <fichero.cc.json[.gz]> [opciones]
|
|
4714
5184
|
codarch diff <base.cc.json[.gz]> <head.cc.json[.gz]> [opciones]
|
|
5185
|
+
codarch serve [ruta] [opciones]
|
|
4715
5186
|
|
|
4716
5187
|
Opciones de analyze:
|
|
4717
5188
|
-o, --out <dir> directorio de salida (por defecto <ruta>/.codarch)
|
|
@@ -4745,6 +5216,14 @@ Opciones de diff:
|
|
|
4745
5216
|
--fail-on <modo> none | cycles | violations | hotspots | any (por defecto none)
|
|
4746
5217
|
-h, --help esta ayuda
|
|
4747
5218
|
|
|
5219
|
+
Opciones de serve:
|
|
5220
|
+
-p, --port <n> puerto local (por defecto 4173)
|
|
5221
|
+
--host <host> interfaz (por defecto 127.0.0.1)
|
|
5222
|
+
--watch reanaliza al cambiar ficheros y refresca por SSE (por defecto)
|
|
5223
|
+
--no-watch solo analiza al arrancar
|
|
5224
|
+
--no-web modo API sin servir la app (para Vite u otro frontend)
|
|
5225
|
+
-h, --help esta ayuda
|
|
5226
|
+
|
|
4748
5227
|
Ejemplos:
|
|
4749
5228
|
codarch analyze .
|
|
4750
5229
|
codarch analyze . -o .codarch --title "Mi proyecto"
|
|
@@ -4752,6 +5231,8 @@ Ejemplos:
|
|
|
4752
5231
|
codarch render samples/codecharta_visualization.cc.json.gz
|
|
4753
5232
|
codarch render mapa.cc.json -o mapa.html --area rloc --color maxComplexity
|
|
4754
5233
|
codarch diff base.cc.json head.cc.json -o report.json --fail-on cycles
|
|
5234
|
+
codarch serve .
|
|
5235
|
+
codarch serve . --no-watch
|
|
4755
5236
|
`;
|
|
4756
5237
|
function parseAnalyze(args) {
|
|
4757
5238
|
const options = { help: false, ignore: [] };
|
|
@@ -5015,6 +5496,63 @@ function parseDiff(args) {
|
|
|
5015
5496
|
function isFailOn(value) {
|
|
5016
5497
|
return value === "none" || value === "cycles" || value === "violations" || value === "hotspots" || value === "any";
|
|
5017
5498
|
}
|
|
5499
|
+
function parseServe(args) {
|
|
5500
|
+
const options = {
|
|
5501
|
+
port: 4173,
|
|
5502
|
+
host: "127.0.0.1",
|
|
5503
|
+
watch: true,
|
|
5504
|
+
web: true,
|
|
5505
|
+
help: false
|
|
5506
|
+
};
|
|
5507
|
+
const positional = [];
|
|
5508
|
+
for (let index = 0; index < args.length; index++) {
|
|
5509
|
+
const arg = args[index];
|
|
5510
|
+
if (arg === "--port" || arg === "-p") {
|
|
5511
|
+
const value = args[++index];
|
|
5512
|
+
const port = Number(value);
|
|
5513
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
5514
|
+
console.error("--port necesita un n\xFAmero entre 1 y 65535");
|
|
5515
|
+
return 1;
|
|
5516
|
+
}
|
|
5517
|
+
options.port = port;
|
|
5518
|
+
} else if (arg.startsWith("--port=")) {
|
|
5519
|
+
const port = Number(arg.slice("--port=".length));
|
|
5520
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
5521
|
+
console.error("--port necesita un n\xFAmero entre 1 y 65535");
|
|
5522
|
+
return 1;
|
|
5523
|
+
}
|
|
5524
|
+
options.port = port;
|
|
5525
|
+
} else if (arg === "--host") {
|
|
5526
|
+
const value = args[++index];
|
|
5527
|
+
if (!value) {
|
|
5528
|
+
console.error("--host necesita un valor");
|
|
5529
|
+
return 1;
|
|
5530
|
+
}
|
|
5531
|
+
options.host = value;
|
|
5532
|
+
} else if (arg.startsWith("--host=")) {
|
|
5533
|
+
options.host = arg.slice("--host=".length);
|
|
5534
|
+
} else if (arg === "--watch") {
|
|
5535
|
+
options.watch = true;
|
|
5536
|
+
} else if (arg === "--no-watch") {
|
|
5537
|
+
options.watch = false;
|
|
5538
|
+
} else if (arg === "--no-web") {
|
|
5539
|
+
options.web = false;
|
|
5540
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
5541
|
+
options.help = true;
|
|
5542
|
+
} else if (arg.startsWith("-")) {
|
|
5543
|
+
console.error(`opci\xF3n desconocida en serve: ${arg}`);
|
|
5544
|
+
return 1;
|
|
5545
|
+
} else {
|
|
5546
|
+
positional.push(arg);
|
|
5547
|
+
}
|
|
5548
|
+
}
|
|
5549
|
+
if (positional.length > 1) {
|
|
5550
|
+
console.error("serve acepta como m\xE1ximo una ruta");
|
|
5551
|
+
return 1;
|
|
5552
|
+
}
|
|
5553
|
+
options.root = positional[0];
|
|
5554
|
+
return options;
|
|
5555
|
+
}
|
|
5018
5556
|
function main(argv) {
|
|
5019
5557
|
const [command, ...rest] = argv;
|
|
5020
5558
|
if (!command || command === "--help" || command === "-h") {
|
|
@@ -5045,6 +5583,15 @@ function main(argv) {
|
|
|
5045
5583
|
if (typeof parsed === "number") return parsed;
|
|
5046
5584
|
return runDiff(parsed);
|
|
5047
5585
|
}
|
|
5586
|
+
if (command === "serve") {
|
|
5587
|
+
const parsed = parseServe(rest);
|
|
5588
|
+
if (typeof parsed === "number") return parsed;
|
|
5589
|
+
if (parsed.help) {
|
|
5590
|
+
console.log(USAGE);
|
|
5591
|
+
return 0;
|
|
5592
|
+
}
|
|
5593
|
+
return runServe(parsed);
|
|
5594
|
+
}
|
|
5048
5595
|
console.error(`comando desconocido: ${command}
|
|
5049
5596
|
${USAGE}`);
|
|
5050
5597
|
return 1;
|