@uniformdev/transformer 1.1.7 → 1.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +242 -40
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -900,7 +900,7 @@ var PatternAnalyzerService = class {
|
|
|
900
900
|
this.logger = logger;
|
|
901
901
|
}
|
|
902
902
|
async analyze(options) {
|
|
903
|
-
const { rootDir, compositionsDir, projectMapNodesDir, minGroupSize, depth, threshold } = options;
|
|
903
|
+
const { rootDir, compositionsDir, projectMapNodesDir, minGroupSize, depth, threshold, subset } = options;
|
|
904
904
|
const compositionsPath = this.fileSystem.resolvePath(rootDir, compositionsDir);
|
|
905
905
|
const projectMapNodesPath = this.fileSystem.resolvePath(rootDir, projectMapNodesDir);
|
|
906
906
|
const projectMapNodes = await this.loadProjectMapNodes(projectMapNodesPath);
|
|
@@ -926,7 +926,9 @@ var PatternAnalyzerService = class {
|
|
|
926
926
|
}
|
|
927
927
|
}
|
|
928
928
|
let groups;
|
|
929
|
-
if (
|
|
929
|
+
if (subset) {
|
|
930
|
+
groups = this.clusterBySubset(loadedCompositions, depth);
|
|
931
|
+
} else if (threshold === 0) {
|
|
930
932
|
const fingerprintGroups = /* @__PURE__ */ new Map();
|
|
931
933
|
for (const item of loadedCompositions) {
|
|
932
934
|
if (!fingerprintGroups.has(item.fingerprint)) {
|
|
@@ -960,34 +962,77 @@ var PatternAnalyzerService = class {
|
|
|
960
962
|
}
|
|
961
963
|
continue;
|
|
962
964
|
}
|
|
963
|
-
|
|
964
|
-
const
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
965
|
+
if (subset) {
|
|
966
|
+
const nodeCache = /* @__PURE__ */ new Map();
|
|
967
|
+
for (const item of group) {
|
|
968
|
+
const id = item.composition.composition._id ?? "";
|
|
969
|
+
nodeCache.set(id, this.parseCompositionToNode(item.composition, depth));
|
|
970
|
+
}
|
|
971
|
+
group.sort((a, b) => {
|
|
972
|
+
const idA = a.composition.composition._id ?? "";
|
|
973
|
+
const idB = b.composition.composition._id ?? "";
|
|
974
|
+
const countA = this.countNodes(nodeCache.get(idA));
|
|
975
|
+
const countB = this.countNodes(nodeCache.get(idB));
|
|
976
|
+
if (countB !== countA) return countB - countA;
|
|
977
|
+
return idA.localeCompare(idB);
|
|
978
|
+
});
|
|
979
|
+
let mergedNode = nodeCache.get(group[0].composition.composition._id ?? "");
|
|
980
|
+
for (let i = 1; i < group.length; i++) {
|
|
981
|
+
const id = group[i].composition.composition._id ?? "";
|
|
982
|
+
mergedNode = this.mergeComponentNodes(mergedNode, nodeCache.get(id));
|
|
983
|
+
}
|
|
984
|
+
const firstComposition = group[0].composition;
|
|
985
|
+
const rootType = firstComposition.composition.type;
|
|
986
|
+
const firstName = this.getCompositionName(firstComposition);
|
|
987
|
+
rootTypeCounts.set(rootType, (rootTypeCounts.get(rootType) ?? 0) + 1);
|
|
988
|
+
const structureDescription = this.generateStructureDescriptionFromNode(mergedNode);
|
|
989
|
+
const compositions = group.map(({ composition, filePath }) => ({
|
|
990
|
+
id: composition.composition._id ?? "unknown",
|
|
991
|
+
name: this.getCompositionName(composition),
|
|
992
|
+
projectMapNodeSlug: this.resolveProjectMapSlug(composition, projectMapNodes),
|
|
993
|
+
filePath
|
|
994
|
+
}));
|
|
995
|
+
const fingerprint = group[0].fingerprint;
|
|
996
|
+
const patternGroup = {
|
|
997
|
+
name: rootType,
|
|
998
|
+
structureDescription,
|
|
999
|
+
fingerprint,
|
|
1000
|
+
compositions,
|
|
1001
|
+
_rootType: rootType,
|
|
1002
|
+
_firstName: firstName
|
|
1003
|
+
};
|
|
1004
|
+
patterns.push(patternGroup);
|
|
1005
|
+
patternCompositionsData.set(patternGroup, group.map((g) => g.composition));
|
|
1006
|
+
} else {
|
|
1007
|
+
group.sort((a, b) => {
|
|
1008
|
+
const idA = a.composition.composition._id ?? "";
|
|
1009
|
+
const idB = b.composition.composition._id ?? "";
|
|
1010
|
+
return idA.localeCompare(idB);
|
|
1011
|
+
});
|
|
1012
|
+
const firstComposition = group[0].composition;
|
|
1013
|
+
const rootType = firstComposition.composition.type;
|
|
1014
|
+
const firstName = this.getCompositionName(firstComposition);
|
|
1015
|
+
rootTypeCounts.set(rootType, (rootTypeCounts.get(rootType) ?? 0) + 1);
|
|
1016
|
+
const structureDescription = this.generateStructureDescription(firstComposition);
|
|
1017
|
+
const compositions = group.map(({ composition, filePath }) => ({
|
|
1018
|
+
id: composition.composition._id ?? "unknown",
|
|
1019
|
+
name: this.getCompositionName(composition),
|
|
1020
|
+
projectMapNodeSlug: this.resolveProjectMapSlug(composition, projectMapNodes),
|
|
1021
|
+
filePath
|
|
1022
|
+
}));
|
|
1023
|
+
const fingerprint = group[0].fingerprint;
|
|
1024
|
+
const patternGroup = {
|
|
1025
|
+
name: rootType,
|
|
1026
|
+
// Temporary name, will be updated below if disambiguation needed
|
|
1027
|
+
structureDescription,
|
|
1028
|
+
fingerprint,
|
|
1029
|
+
compositions,
|
|
1030
|
+
_rootType: rootType,
|
|
1031
|
+
_firstName: firstName
|
|
1032
|
+
};
|
|
1033
|
+
patterns.push(patternGroup);
|
|
1034
|
+
patternCompositionsData.set(patternGroup, group.map((g) => g.composition));
|
|
1035
|
+
}
|
|
991
1036
|
}
|
|
992
1037
|
for (const pattern of patterns) {
|
|
993
1038
|
const p = pattern;
|
|
@@ -1098,8 +1143,7 @@ var PatternAnalyzerService = class {
|
|
|
1098
1143
|
const sortedSlotNames = Object.keys(slots).sort();
|
|
1099
1144
|
for (const slotName of sortedSlotNames) {
|
|
1100
1145
|
const instances = slots[slotName];
|
|
1101
|
-
if (!Array.isArray(instances)) {
|
|
1102
|
-
node.slots.set(slotName, []);
|
|
1146
|
+
if (!Array.isArray(instances) || instances.length === 0) {
|
|
1103
1147
|
continue;
|
|
1104
1148
|
}
|
|
1105
1149
|
const nextDepth = remainingDepth === -1 ? -1 : remainingDepth - 1;
|
|
@@ -1138,6 +1182,158 @@ var PatternAnalyzerService = class {
|
|
|
1138
1182
|
}
|
|
1139
1183
|
return distance;
|
|
1140
1184
|
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Checks if `smaller` is a structural subset of `larger`.
|
|
1187
|
+
* Same root type required. For each slot in `smaller`, that slot must exist
|
|
1188
|
+
* in `larger` with at least the same components at the same positions.
|
|
1189
|
+
* `larger` may have extra slots or extra trailing components. Recursive.
|
|
1190
|
+
*/
|
|
1191
|
+
isSubsetOf(smaller, larger) {
|
|
1192
|
+
if (smaller.type !== larger.type) {
|
|
1193
|
+
return false;
|
|
1194
|
+
}
|
|
1195
|
+
for (const [slotName, smallerChildren] of smaller.slots) {
|
|
1196
|
+
const largerChildren = larger.slots.get(slotName);
|
|
1197
|
+
if (!largerChildren) {
|
|
1198
|
+
return false;
|
|
1199
|
+
}
|
|
1200
|
+
if (smallerChildren.length > largerChildren.length) {
|
|
1201
|
+
return false;
|
|
1202
|
+
}
|
|
1203
|
+
for (let i = 0; i < smallerChildren.length; i++) {
|
|
1204
|
+
if (!this.isSubsetOf(smallerChildren[i], largerChildren[i])) {
|
|
1205
|
+
return false;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
return true;
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Counts total component instances in a tree (root + all descendants).
|
|
1213
|
+
*/
|
|
1214
|
+
countNodes(node) {
|
|
1215
|
+
let count = 1;
|
|
1216
|
+
for (const children of node.slots.values()) {
|
|
1217
|
+
for (const child of children) {
|
|
1218
|
+
count += this.countNodes(child);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
return count;
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Merges two ComponentNode trees into a combined superset structure.
|
|
1225
|
+
* Takes the union of all slots and the maximum components at each position.
|
|
1226
|
+
* Both nodes must have the same root type.
|
|
1227
|
+
*/
|
|
1228
|
+
mergeComponentNodes(a, b) {
|
|
1229
|
+
if (a.type !== b.type) {
|
|
1230
|
+
return a;
|
|
1231
|
+
}
|
|
1232
|
+
const mergedSlots = /* @__PURE__ */ new Map();
|
|
1233
|
+
const allSlotNames = /* @__PURE__ */ new Set([...a.slots.keys(), ...b.slots.keys()]);
|
|
1234
|
+
for (const slotName of allSlotNames) {
|
|
1235
|
+
const aChildren = a.slots.get(slotName) ?? [];
|
|
1236
|
+
const bChildren = b.slots.get(slotName) ?? [];
|
|
1237
|
+
const maxLen = Math.max(aChildren.length, bChildren.length);
|
|
1238
|
+
const mergedChildren = [];
|
|
1239
|
+
for (let i = 0; i < maxLen; i++) {
|
|
1240
|
+
const aChild = aChildren[i];
|
|
1241
|
+
const bChild = bChildren[i];
|
|
1242
|
+
if (aChild && bChild) {
|
|
1243
|
+
if (aChild.type === bChild.type) {
|
|
1244
|
+
mergedChildren.push(this.mergeComponentNodes(aChild, bChild));
|
|
1245
|
+
} else {
|
|
1246
|
+
mergedChildren.push(this.countNodes(aChild) >= this.countNodes(bChild) ? aChild : bChild);
|
|
1247
|
+
}
|
|
1248
|
+
} else {
|
|
1249
|
+
mergedChildren.push(aChild ?? bChild);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
mergedSlots.set(slotName, mergedChildren);
|
|
1253
|
+
}
|
|
1254
|
+
return { type: a.type, slots: mergedSlots };
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Clusters compositions using Union-Find based on subset relationship.
|
|
1258
|
+
* Two compositions are unioned if either is a structural subset of the other.
|
|
1259
|
+
*/
|
|
1260
|
+
clusterBySubset(compositions, depth) {
|
|
1261
|
+
const n = compositions.length;
|
|
1262
|
+
if (n === 0) return [];
|
|
1263
|
+
const parsedNodes = compositions.map(
|
|
1264
|
+
(item) => this.parseCompositionToNode(item.composition, depth)
|
|
1265
|
+
);
|
|
1266
|
+
const parent = Array.from({ length: n }, (_, i) => i);
|
|
1267
|
+
const rank = Array(n).fill(0);
|
|
1268
|
+
const find = (x) => {
|
|
1269
|
+
if (parent[x] !== x) {
|
|
1270
|
+
parent[x] = find(parent[x]);
|
|
1271
|
+
}
|
|
1272
|
+
return parent[x];
|
|
1273
|
+
};
|
|
1274
|
+
const union = (x, y) => {
|
|
1275
|
+
const rootX = find(x);
|
|
1276
|
+
const rootY = find(y);
|
|
1277
|
+
if (rootX !== rootY) {
|
|
1278
|
+
if (rank[rootX] < rank[rootY]) {
|
|
1279
|
+
parent[rootX] = rootY;
|
|
1280
|
+
} else if (rank[rootX] > rank[rootY]) {
|
|
1281
|
+
parent[rootY] = rootX;
|
|
1282
|
+
} else {
|
|
1283
|
+
parent[rootY] = rootX;
|
|
1284
|
+
rank[rootX]++;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
for (let i = 0; i < n; i++) {
|
|
1289
|
+
for (let j = i + 1; j < n; j++) {
|
|
1290
|
+
if (parsedNodes[i].type !== parsedNodes[j].type) {
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
if (this.isSubsetOf(parsedNodes[i], parsedNodes[j]) || this.isSubsetOf(parsedNodes[j], parsedNodes[i])) {
|
|
1294
|
+
union(i, j);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1299
|
+
for (let i = 0; i < n; i++) {
|
|
1300
|
+
const root = find(i);
|
|
1301
|
+
if (!groups.has(root)) {
|
|
1302
|
+
groups.set(root, []);
|
|
1303
|
+
}
|
|
1304
|
+
groups.get(root).push(compositions[i]);
|
|
1305
|
+
}
|
|
1306
|
+
return Array.from(groups.values());
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Generates a structure description from a ComponentNode tree (instead of a Composition).
|
|
1310
|
+
*/
|
|
1311
|
+
generateStructureDescriptionFromNode(node) {
|
|
1312
|
+
return this.generateNodeDescriptionFromNode(node, 0);
|
|
1313
|
+
}
|
|
1314
|
+
generateNodeDescriptionFromNode(node, depth) {
|
|
1315
|
+
if (node.slots.size === 0) {
|
|
1316
|
+
return node.type;
|
|
1317
|
+
}
|
|
1318
|
+
const sortedSlotNames = [...node.slots.keys()].sort();
|
|
1319
|
+
const slotDescriptions = sortedSlotNames.map((slotName) => {
|
|
1320
|
+
const children = node.slots.get(slotName);
|
|
1321
|
+
if (children.length === 0) {
|
|
1322
|
+
return null;
|
|
1323
|
+
}
|
|
1324
|
+
const instanceTypes = children.map((child) => {
|
|
1325
|
+
if (child.slots.size > 0) {
|
|
1326
|
+
return this.generateNodeDescriptionFromNode(child, depth + 1);
|
|
1327
|
+
}
|
|
1328
|
+
return child.type;
|
|
1329
|
+
});
|
|
1330
|
+
return `${slotName}[${instanceTypes.join(", ")}]`;
|
|
1331
|
+
}).filter(Boolean);
|
|
1332
|
+
if (slotDescriptions.length === 0) {
|
|
1333
|
+
return node.type;
|
|
1334
|
+
}
|
|
1335
|
+
return `${node.type} > ${slotDescriptions.join(" > ")}`;
|
|
1336
|
+
}
|
|
1141
1337
|
/**
|
|
1142
1338
|
* Extracts all unique component types from a composition recursively.
|
|
1143
1339
|
*/
|
|
@@ -1255,17 +1451,21 @@ var PatternAnalyzerService = class {
|
|
|
1255
1451
|
return type;
|
|
1256
1452
|
}
|
|
1257
1453
|
const sortedSlotNames = Object.keys(slots).sort();
|
|
1258
|
-
const slotFingerprints =
|
|
1454
|
+
const slotFingerprints = [];
|
|
1455
|
+
for (const slotName of sortedSlotNames) {
|
|
1259
1456
|
const instances = slots[slotName];
|
|
1260
1457
|
if (!Array.isArray(instances) || instances.length === 0) {
|
|
1261
|
-
|
|
1458
|
+
continue;
|
|
1262
1459
|
}
|
|
1263
1460
|
const nextDepth = remainingDepth === -1 ? -1 : remainingDepth - 1;
|
|
1264
1461
|
const instanceFingerprints = instances.map(
|
|
1265
1462
|
(instance) => this.generateNodeFingerprint(instance.type, instance.slots, nextDepth)
|
|
1266
1463
|
);
|
|
1267
|
-
|
|
1268
|
-
}
|
|
1464
|
+
slotFingerprints.push(`${slotName}:[${instanceFingerprints.join(",")}]`);
|
|
1465
|
+
}
|
|
1466
|
+
if (slotFingerprints.length === 0) {
|
|
1467
|
+
return type;
|
|
1468
|
+
}
|
|
1269
1469
|
return `${type}{${slotFingerprints.join(";")}}`;
|
|
1270
1470
|
}
|
|
1271
1471
|
/**
|
|
@@ -1583,7 +1783,7 @@ function createFindCompositionPatternCandidatesCommand() {
|
|
|
1583
1783
|
"--threshold <count>",
|
|
1584
1784
|
"Number of component differences allowed while still matching (0 = exact match)",
|
|
1585
1785
|
"0"
|
|
1586
|
-
).option("--format <format>", "Output format: text or json", "text").option("--brief", "Skip structure and composition details in text output").hook("preAction", (thisCommand) => {
|
|
1786
|
+
).option("--format <format>", "Output format: text or json", "text").option("--brief", "Skip structure and composition details in text output").option("--subset", "Group compositions where one structure is a subset of another", false).hook("preAction", (thisCommand) => {
|
|
1587
1787
|
const opts = thisCommand.opts();
|
|
1588
1788
|
const minGroupSize = parseInt(opts.minGroupSize, 10);
|
|
1589
1789
|
if (isNaN(minGroupSize) || minGroupSize < 2) {
|
|
@@ -1612,7 +1812,8 @@ function createFindCompositionPatternCandidatesCommand() {
|
|
|
1612
1812
|
depth: parseInt(opts.depth, 10),
|
|
1613
1813
|
threshold: parseInt(opts.threshold, 10),
|
|
1614
1814
|
format: opts.format,
|
|
1615
|
-
brief: opts.brief ?? false
|
|
1815
|
+
brief: opts.brief ?? false,
|
|
1816
|
+
subset: opts.subset ?? false
|
|
1616
1817
|
};
|
|
1617
1818
|
const logger = new Logger();
|
|
1618
1819
|
const fileSystem = new FileSystemService();
|
|
@@ -1627,7 +1828,8 @@ function createFindCompositionPatternCandidatesCommand() {
|
|
|
1627
1828
|
minGroupSize: options.minGroupSize,
|
|
1628
1829
|
depth: options.depth,
|
|
1629
1830
|
threshold: options.threshold,
|
|
1630
|
-
strict: options.strict ?? false
|
|
1831
|
+
strict: options.strict ?? false,
|
|
1832
|
+
subset: options.subset ?? false
|
|
1631
1833
|
});
|
|
1632
1834
|
if (options.format === "json") {
|
|
1633
1835
|
console.log(analyzer.formatJsonOutput(result));
|
|
@@ -3375,7 +3577,7 @@ function createPropagateRootComponentSlotCommand() {
|
|
|
3375
3577
|
// package.json
|
|
3376
3578
|
var package_default = {
|
|
3377
3579
|
name: "@uniformdev/transformer",
|
|
3378
|
-
version: "1.1.
|
|
3580
|
+
version: "1.1.9",
|
|
3379
3581
|
description: "CLI tool for transforming Uniform.dev serialization files offline",
|
|
3380
3582
|
type: "module",
|
|
3381
3583
|
bin: {
|