@uniformdev/transformer 1.1.8 → 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 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 (threshold === 0) {
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
- group.sort((a, b) => {
964
- const idA = a.composition.composition._id ?? "";
965
- const idB = b.composition.composition._id ?? "";
966
- return idA.localeCompare(idB);
967
- });
968
- const firstComposition = group[0].composition;
969
- const rootType = firstComposition.composition.type;
970
- const firstName = this.getCompositionName(firstComposition);
971
- rootTypeCounts.set(rootType, (rootTypeCounts.get(rootType) ?? 0) + 1);
972
- const structureDescription = this.generateStructureDescription(firstComposition);
973
- const compositions = group.map(({ composition, filePath }) => ({
974
- id: composition.composition._id ?? "unknown",
975
- name: this.getCompositionName(composition),
976
- projectMapNodeSlug: this.resolveProjectMapSlug(composition, projectMapNodes),
977
- filePath
978
- }));
979
- const fingerprint = group[0].fingerprint;
980
- const patternGroup = {
981
- name: rootType,
982
- // Temporary name, will be updated below if disambiguation needed
983
- structureDescription,
984
- fingerprint,
985
- compositions,
986
- _rootType: rootType,
987
- _firstName: firstName
988
- };
989
- patterns.push(patternGroup);
990
- patternCompositionsData.set(patternGroup, group.map((g) => g.composition));
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;
@@ -1137,6 +1182,158 @@ var PatternAnalyzerService = class {
1137
1182
  }
1138
1183
  return distance;
1139
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
+ }
1140
1337
  /**
1141
1338
  * Extracts all unique component types from a composition recursively.
1142
1339
  */
@@ -1586,7 +1783,7 @@ function createFindCompositionPatternCandidatesCommand() {
1586
1783
  "--threshold <count>",
1587
1784
  "Number of component differences allowed while still matching (0 = exact match)",
1588
1785
  "0"
1589
- ).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) => {
1590
1787
  const opts = thisCommand.opts();
1591
1788
  const minGroupSize = parseInt(opts.minGroupSize, 10);
1592
1789
  if (isNaN(minGroupSize) || minGroupSize < 2) {
@@ -1615,7 +1812,8 @@ function createFindCompositionPatternCandidatesCommand() {
1615
1812
  depth: parseInt(opts.depth, 10),
1616
1813
  threshold: parseInt(opts.threshold, 10),
1617
1814
  format: opts.format,
1618
- brief: opts.brief ?? false
1815
+ brief: opts.brief ?? false,
1816
+ subset: opts.subset ?? false
1619
1817
  };
1620
1818
  const logger = new Logger();
1621
1819
  const fileSystem = new FileSystemService();
@@ -1630,7 +1828,8 @@ function createFindCompositionPatternCandidatesCommand() {
1630
1828
  minGroupSize: options.minGroupSize,
1631
1829
  depth: options.depth,
1632
1830
  threshold: options.threshold,
1633
- strict: options.strict ?? false
1831
+ strict: options.strict ?? false,
1832
+ subset: options.subset ?? false
1634
1833
  });
1635
1834
  if (options.format === "json") {
1636
1835
  console.log(analyzer.formatJsonOutput(result));
@@ -3378,7 +3577,7 @@ function createPropagateRootComponentSlotCommand() {
3378
3577
  // package.json
3379
3578
  var package_default = {
3380
3579
  name: "@uniformdev/transformer",
3381
- version: "1.1.8",
3580
+ version: "1.1.9",
3382
3581
  description: "CLI tool for transforming Uniform.dev serialization files offline",
3383
3582
  type: "module",
3384
3583
  bin: {