@uniformdev/transformer 1.1.39 → 1.1.41

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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/index.ts
4
- import { Command as Command18 } from "commander";
4
+ import { Command as Command20 } from "commander";
5
5
 
6
6
  // src/cli/commands/propagate-root-component-property.ts
7
7
  import { Command } from "commander";
@@ -1611,6 +1611,11 @@ function walkAndRegenerate(value, currentPath) {
1611
1611
  return value;
1612
1612
  }
1613
1613
 
1614
+ // src/core/utils.ts
1615
+ function splitList(value) {
1616
+ return value.split(/[,;|]/).map((s) => s.trim()).filter((s) => s.length > 0);
1617
+ }
1618
+
1614
1619
  // src/core/services/property-propagator.service.ts
1615
1620
  var PropertyPropagatorService = class {
1616
1621
  constructor(fileSystem, componentService, compositionService, logger) {
@@ -1654,7 +1659,7 @@ var PropertyPropagatorService = class {
1654
1659
  this.logger.info(`Loading component: ${targetComponentType}`);
1655
1660
  const { component: targetComponent, filePath: targetFilePath } = await this.componentService.loadComponent(fullComponentsDir, targetComponentType, findOptions);
1656
1661
  this.logger.debug(`Loaded target component "${targetComponentType}" from ${targetFilePath}`);
1657
- const propertyNames = property.split("|").map((p) => p.trim()).filter((p) => p.length > 0);
1662
+ const propertyNames = splitList(property);
1658
1663
  const resolvedParams = [];
1659
1664
  const resolvedNames = [];
1660
1665
  for (const { sourceType, sourceComponent } of sourceComponents) {
@@ -1892,7 +1897,7 @@ var PropertyPropagatorService = class {
1892
1897
  };
1893
1898
  }
1894
1899
  parsePipeSeparatedValues(value, strict) {
1895
- const entries = value.split("|").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
1900
+ const entries = splitList(value);
1896
1901
  const normalized = [];
1897
1902
  for (const entry of entries) {
1898
1903
  const exists = normalized.some(
@@ -3850,7 +3855,7 @@ var ComponentAdderService = class {
3850
3855
  return id1.toLowerCase() === id2.toLowerCase();
3851
3856
  }
3852
3857
  parseParentComponentTypes(parentComponentType) {
3853
- return parentComponentType.split("|").map((t) => t.trim()).filter((t) => t.length > 0);
3858
+ return splitList(parentComponentType);
3854
3859
  }
3855
3860
  matchesAnyParentType(instanceType, parentTypes, strict) {
3856
3861
  return parentTypes.some((pt) => this.compareIds(instanceType, pt, strict));
@@ -4123,8 +4128,7 @@ var ComponentAdderService = class {
4123
4128
  }
4124
4129
  const newInstance = {
4125
4130
  type: componentTypeInPattern,
4126
- _pattern: pattern.componentPatternId,
4127
- _id: randomUUID()
4131
+ _pattern: pattern.componentPatternId
4128
4132
  };
4129
4133
  const compositionsResult = await this.addComponentToDirectory(
4130
4134
  fullCompositionsDir,
@@ -4238,13 +4242,19 @@ var ComponentAdderService = class {
4238
4242
  continue;
4239
4243
  }
4240
4244
  const rootInstance = composition.composition;
4245
+ const rootPath = `${rootInstance._id ?? ""}${rootInstance.type}`;
4241
4246
  let modified = false;
4242
4247
  if (this.matchesAnyParentType(rootInstance.type, parentTypes, strict)) {
4243
4248
  if (!rootInstance.slots) {
4244
4249
  rootInstance.slots = {};
4245
4250
  }
4246
4251
  const instanceCopy = JSON.parse(JSON.stringify(newInstance));
4247
- this.regenerateInstanceIds(instanceCopy);
4252
+ if (instanceCopy._pattern !== void 0) {
4253
+ const slotIndex = rootInstance.slots[slot]?.length ?? 0;
4254
+ instanceCopy._id = computeGuidHash(`${rootPath}.${slot}[${slotIndex}]${instanceCopy.type}`);
4255
+ } else {
4256
+ this.regenerateInstanceIds(instanceCopy);
4257
+ }
4248
4258
  this.addComponentToSlot(rootInstance.slots, slot, instanceCopy);
4249
4259
  modified = true;
4250
4260
  }
@@ -4254,7 +4264,8 @@ var ComponentAdderService = class {
4254
4264
  parentTypes,
4255
4265
  slot,
4256
4266
  newInstance,
4257
- strict
4267
+ strict,
4268
+ rootPath
4258
4269
  );
4259
4270
  if (nestedModified) {
4260
4271
  modified = true;
@@ -4285,17 +4296,24 @@ var ComponentAdderService = class {
4285
4296
  }
4286
4297
  return filesModified;
4287
4298
  }
4288
- addComponentToNestedSlots(slots, parentTypes, slot, newInstance, strict) {
4299
+ addComponentToNestedSlots(slots, parentTypes, slot, newInstance, strict, pathPrefix) {
4289
4300
  let modified = false;
4290
- for (const slotInstances of Object.values(slots)) {
4301
+ for (const [slotName, slotInstances] of Object.entries(slots)) {
4291
4302
  if (!Array.isArray(slotInstances)) continue;
4292
- for (const instance of slotInstances) {
4303
+ for (let i = 0; i < slotInstances.length; i++) {
4304
+ const instance = slotInstances[i];
4305
+ const instancePath = `${pathPrefix}.${slotName}[${i}]${instance.type}`;
4293
4306
  if (this.matchesAnyParentType(instance.type, parentTypes, strict)) {
4294
4307
  if (!instance.slots) {
4295
4308
  instance.slots = {};
4296
4309
  }
4297
4310
  const instanceCopy = JSON.parse(JSON.stringify(newInstance));
4298
- this.regenerateInstanceIds(instanceCopy);
4311
+ if (instanceCopy._pattern !== void 0) {
4312
+ const slotIndex = instance.slots[slot]?.length ?? 0;
4313
+ instanceCopy._id = computeGuidHash(`${instancePath}.${slot}[${slotIndex}]${instanceCopy.type}`);
4314
+ } else {
4315
+ this.regenerateInstanceIds(instanceCopy);
4316
+ }
4299
4317
  this.addComponentToSlot(instance.slots, slot, instanceCopy);
4300
4318
  modified = true;
4301
4319
  }
@@ -4305,7 +4323,8 @@ var ComponentAdderService = class {
4305
4323
  parentTypes,
4306
4324
  slot,
4307
4325
  newInstance,
4308
- strict
4326
+ strict,
4327
+ instancePath
4309
4328
  );
4310
4329
  if (nestedModified) {
4311
4330
  modified = true;
@@ -4480,7 +4499,7 @@ var SlotPropagatorService = class {
4480
4499
  }
4481
4500
  this.logger.info(`Loading component: ${targetComponentType}`);
4482
4501
  const { component: targetComponent, filePath: targetFilePath } = await this.componentService.loadComponent(fullComponentsDir, targetComponentType, findOptions);
4483
- const slotNames = slot.split("|").map((s) => s.trim()).filter((s) => s.length > 0);
4502
+ const slotNames = splitList(slot);
4484
4503
  const resolvedSlots = [];
4485
4504
  const resolvedSlotIds = [];
4486
4505
  for (const { sourceType, sourceComponent } of sourceComponents) {
@@ -4655,7 +4674,7 @@ var SlotPropagatorService = class {
4655
4674
  instance.slots[slotName].push(...components);
4656
4675
  }
4657
4676
  parsePipeSeparatedValues(value, strict) {
4658
- const entries = value.split("|").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
4677
+ const entries = splitList(value);
4659
4678
  const normalized = [];
4660
4679
  for (const entry of entries) {
4661
4680
  const exists = normalized.some(
@@ -4802,8 +4821,8 @@ function createConvertCompositionsToEntriesCommand() {
4802
4821
  logger
4803
4822
  );
4804
4823
  try {
4805
- const compositionTypes = options.compositionTypes.split("|").map((t) => t.trim()).filter((t) => t.length > 0);
4806
- const parsePipeSeparated = (value) => value ? [...new Set(value.split("|").map((t) => t.trim()).filter((t) => t.length > 0))] : [];
4824
+ const compositionTypes = splitList(options.compositionTypes);
4825
+ const parsePipeSeparated = (value) => value ? [...new Set(splitList(value))] : [];
4807
4826
  const componentsToReferences = parsePipeSeparated(options.componentsToReferences);
4808
4827
  const componentsToBlocks = parsePipeSeparated(options.componentsToBlocks);
4809
4828
  const slotsToReferences = parsePipeSeparated(options.slotsToReferences);
@@ -5115,7 +5134,7 @@ function createRemoveParameterCommand() {
5115
5134
  const fileSystem = new FileSystemService();
5116
5135
  const componentService = new ComponentService(fileSystem);
5117
5136
  const remover = new ParameterRemoverService(fileSystem, componentService, logger);
5118
- const rawTypes = options.componentType.split("|").map((t) => t.trim()).filter(Boolean);
5137
+ const rawTypes = splitList(options.componentType);
5119
5138
  const hasWildcard = rawTypes.includes("*");
5120
5139
  let componentTypes;
5121
5140
  if (hasWildcard) {
@@ -5426,7 +5445,7 @@ function createRemoveFieldCommand() {
5426
5445
  const fileSystem = new FileSystemService();
5427
5446
  const componentService = new ComponentService(fileSystem);
5428
5447
  const remover = new FieldRemoverService(fileSystem, componentService, logger);
5429
- const rawTypes = options.componentType.split("|").map((t) => t.trim()).filter(Boolean);
5448
+ const rawTypes = splitList(options.componentType);
5430
5449
  const componentTypes = rawTypes.includes("*") ? ["*"] : rawTypes;
5431
5450
  const aggregate = {
5432
5451
  compositionsModified: 0,
@@ -6353,7 +6372,7 @@ function createRemoveOrphanEntriesCommand() {
6353
6372
  };
6354
6373
  const logger = new Logger();
6355
6374
  logger.info(`rootContentTypes: ${options.rootContentTypes}`);
6356
- const rootContentTypes = options.rootContentTypes.split("|").map((t) => t.trim()).filter(Boolean);
6375
+ const rootContentTypes = splitList(options.rootContentTypes);
6357
6376
  const fileSystem = new FileSystemService();
6358
6377
  const service = new OrphanEntryRemoverService(fileSystem, logger);
6359
6378
  const result = await service.remove({
@@ -6380,7 +6399,16 @@ var UnusedContentTypeRemoverService = class {
6380
6399
  this.logger = logger;
6381
6400
  }
6382
6401
  async remove(options) {
6383
- const { rootDir, contentTypesDir, entriesDir, whatIf, strict } = options;
6402
+ const {
6403
+ rootDir,
6404
+ contentTypesDir,
6405
+ entriesDir,
6406
+ compositionsDir,
6407
+ compositionPatternsDir,
6408
+ componentPatternsDir,
6409
+ whatIf,
6410
+ strict
6411
+ } = options;
6384
6412
  const contentTypesDirFull = this.fileSystem.resolvePath(rootDir, contentTypesDir);
6385
6413
  const entriesDirFull = this.fileSystem.resolvePath(rootDir, entriesDir);
6386
6414
  const ctDirExists = await this.fileSystem.fileExists(contentTypesDirFull);
@@ -6428,6 +6456,27 @@ var UnusedContentTypeRemoverService = class {
6428
6456
  continue;
6429
6457
  }
6430
6458
  usedTypeIds.add(entryData.entry.type);
6459
+ const fields = entryData.entry.fields;
6460
+ if (fields && typeof fields === "object") {
6461
+ this.extractBlockTypesFromParameters(fields, usedTypeIds);
6462
+ }
6463
+ }
6464
+ }
6465
+ const dirsToScan = [
6466
+ { dir: this.fileSystem.resolvePath(rootDir, compositionsDir), label: compositionsDir },
6467
+ {
6468
+ dir: this.fileSystem.resolvePath(rootDir, compositionPatternsDir),
6469
+ label: compositionPatternsDir
6470
+ },
6471
+ {
6472
+ dir: this.fileSystem.resolvePath(rootDir, componentPatternsDir),
6473
+ label: componentPatternsDir
6474
+ }
6475
+ ];
6476
+ for (const { dir, label } of dirsToScan) {
6477
+ const blockTypes = await this.collectBlockTypesFromDir(dir, label);
6478
+ for (const t of blockTypes) {
6479
+ usedTypeIds.add(t);
6431
6480
  }
6432
6481
  }
6433
6482
  let removedContentTypes = 0;
@@ -6454,6 +6503,73 @@ var UnusedContentTypeRemoverService = class {
6454
6503
  retainedContentTypes
6455
6504
  };
6456
6505
  }
6506
+ async collectBlockTypesFromDir(dir, label) {
6507
+ const types = /* @__PURE__ */ new Set();
6508
+ const dirExists = await this.fileSystem.fileExists(dir);
6509
+ if (!dirExists) return types;
6510
+ const files = await this.fileSystem.findFiles(dir, "**/*.{json,yaml,yml}");
6511
+ for (const filePath of files) {
6512
+ let data;
6513
+ try {
6514
+ data = await this.fileSystem.readFile(filePath);
6515
+ } catch {
6516
+ this.logger.warn(`Could not read ${label} file: ${filePath} \u2014 skipping`);
6517
+ continue;
6518
+ }
6519
+ this.extractBlockTypesFromFile(data, types);
6520
+ }
6521
+ return types;
6522
+ }
6523
+ extractBlockTypesFromFile(data, types) {
6524
+ if (!data || typeof data !== "object") return;
6525
+ const comp = data.composition;
6526
+ if (!comp || typeof comp !== "object") return;
6527
+ const node = comp;
6528
+ this.extractBlockTypesFromNode(node, types);
6529
+ const overrides = node._overrides;
6530
+ if (overrides && typeof overrides === "object") {
6531
+ for (const override of Object.values(overrides)) {
6532
+ if (!override || typeof override !== "object") continue;
6533
+ const params = override.parameters;
6534
+ if (params && typeof params === "object") {
6535
+ this.extractBlockTypesFromParameters(params, types);
6536
+ }
6537
+ }
6538
+ }
6539
+ }
6540
+ extractBlockTypesFromNode(node, types) {
6541
+ const params = node.parameters;
6542
+ if (params && typeof params === "object") {
6543
+ this.extractBlockTypesFromParameters(params, types);
6544
+ }
6545
+ const slots = node.slots;
6546
+ if (slots && typeof slots === "object") {
6547
+ for (const slotInstances of Object.values(slots)) {
6548
+ if (!Array.isArray(slotInstances)) continue;
6549
+ for (const instance of slotInstances) {
6550
+ if (instance && typeof instance === "object") {
6551
+ this.extractBlockTypesFromNode(instance, types);
6552
+ }
6553
+ }
6554
+ }
6555
+ }
6556
+ }
6557
+ extractBlockTypesFromParameters(params, types) {
6558
+ for (const param of Object.values(params)) {
6559
+ if (!param || typeof param !== "object") continue;
6560
+ const p = param;
6561
+ if (p.type === "$block" && Array.isArray(p.value)) {
6562
+ for (const item of p.value) {
6563
+ if (item && typeof item === "object") {
6564
+ const blockType = item.type;
6565
+ if (typeof blockType === "string") {
6566
+ types.add(blockType);
6567
+ }
6568
+ }
6569
+ }
6570
+ }
6571
+ }
6572
+ }
6457
6573
  isUsed(contentTypeId, usedTypeIds, strict) {
6458
6574
  if (strict) {
6459
6575
  return usedTypeIds.has(contentTypeId);
@@ -6484,6 +6600,9 @@ function createRemoveUnusedContentTypesCommand() {
6484
6600
  rootDir: options.rootDir,
6485
6601
  contentTypesDir: options.contentTypesDir,
6486
6602
  entriesDir: options.entriesDir,
6603
+ compositionsDir: options.compositionsDir,
6604
+ compositionPatternsDir: options.compositionPatternsDir,
6605
+ componentPatternsDir: options.componentPatternsDir,
6487
6606
  whatIf: options.whatIf ?? false,
6488
6607
  strict: options.strict ?? false
6489
6608
  });
@@ -6494,10 +6613,400 @@ function createRemoveUnusedContentTypesCommand() {
6494
6613
  return command;
6495
6614
  }
6496
6615
 
6616
+ // src/cli/commands/remove-unused-component-types.ts
6617
+ import { Command as Command18 } from "commander";
6618
+
6619
+ // src/core/services/unused-component-type-remover.service.ts
6620
+ var UnusedComponentTypeRemoverService = class {
6621
+ constructor(fileSystem, logger) {
6622
+ this.fileSystem = fileSystem;
6623
+ this.logger = logger;
6624
+ }
6625
+ async remove(options) {
6626
+ const {
6627
+ rootDir,
6628
+ componentsDir,
6629
+ compositionsDir,
6630
+ compositionPatternsDir,
6631
+ componentPatternsDir,
6632
+ whatIf,
6633
+ strict,
6634
+ excludeComponentTypes = []
6635
+ } = options;
6636
+ const componentsDirFull = this.fileSystem.resolvePath(rootDir, componentsDir);
6637
+ const ctDirExists = await this.fileSystem.fileExists(componentsDirFull);
6638
+ if (!ctDirExists) {
6639
+ this.logger.warn(`Components directory not found: ${componentsDir} \u2014 nothing to do`);
6640
+ return { totalComponentTypes: 0, removedComponentTypes: 0, retainedComponentTypes: 0 };
6641
+ }
6642
+ const componentFiles = await this.fileSystem.findFiles(componentsDirFull, "*.{json,yaml,yml}");
6643
+ if (componentFiles.length === 0) {
6644
+ this.logger.warn("No component files found \u2014 nothing to do");
6645
+ return { totalComponentTypes: 0, removedComponentTypes: 0, retainedComponentTypes: 0 };
6646
+ }
6647
+ this.logger.info(`Found ${componentFiles.length} component file(s)`);
6648
+ const componentMap = /* @__PURE__ */ new Map();
6649
+ for (const filePath of componentFiles) {
6650
+ let comp;
6651
+ try {
6652
+ comp = await this.fileSystem.readFile(filePath);
6653
+ } catch {
6654
+ this.logger.warn(`Could not read component file: ${filePath} \u2014 skipping`);
6655
+ continue;
6656
+ }
6657
+ if (!comp?.id) {
6658
+ this.logger.warn(`Component file missing id: ${filePath} \u2014 skipping`);
6659
+ continue;
6660
+ }
6661
+ componentMap.set(filePath, { filePath, id: comp.id });
6662
+ }
6663
+ const usedTypeIds = /* @__PURE__ */ new Set();
6664
+ await this.collectTypesFromDirectory(
6665
+ this.fileSystem.resolvePath(rootDir, compositionsDir),
6666
+ usedTypeIds,
6667
+ "compositions"
6668
+ );
6669
+ await this.collectTypesFromDirectory(
6670
+ this.fileSystem.resolvePath(rootDir, compositionPatternsDir),
6671
+ usedTypeIds,
6672
+ "composition patterns"
6673
+ );
6674
+ await this.collectTypesFromDirectory(
6675
+ this.fileSystem.resolvePath(rootDir, componentPatternsDir),
6676
+ usedTypeIds,
6677
+ "component patterns"
6678
+ );
6679
+ await this.collectAllowedComponents(componentFiles, usedTypeIds);
6680
+ this.logger.info(`Found ${usedTypeIds.size} referenced component type(s) across all sources`);
6681
+ let removedComponentTypes = 0;
6682
+ let retainedComponentTypes = 0;
6683
+ for (const { filePath, id } of componentMap.values()) {
6684
+ const isUsed = this.isUsed(id, usedTypeIds, strict);
6685
+ const isExcluded = excludeComponentTypes.some(
6686
+ (ex) => strict ? ex === id : ex.toLowerCase() === id.toLowerCase()
6687
+ );
6688
+ if (isUsed || isExcluded) {
6689
+ retainedComponentTypes++;
6690
+ } else {
6691
+ const relPath = this.fileSystem.joinPath(
6692
+ componentsDir,
6693
+ this.fileSystem.getBasename(filePath)
6694
+ );
6695
+ this.logger.action(whatIf, "DELETE", relPath);
6696
+ if (!whatIf) {
6697
+ this.fileSystem.deleteFile(filePath);
6698
+ }
6699
+ removedComponentTypes++;
6700
+ }
6701
+ }
6702
+ return {
6703
+ totalComponentTypes: componentMap.size,
6704
+ removedComponentTypes,
6705
+ retainedComponentTypes
6706
+ };
6707
+ }
6708
+ async collectTypesFromDirectory(dirPath, usedTypeIds, label) {
6709
+ const dirExists = await this.fileSystem.fileExists(dirPath);
6710
+ if (!dirExists) {
6711
+ return;
6712
+ }
6713
+ let files;
6714
+ try {
6715
+ files = await this.fileSystem.findFiles(dirPath, "**/*.{json,yaml,yml}");
6716
+ } catch {
6717
+ return;
6718
+ }
6719
+ if (files.length === 0) {
6720
+ return;
6721
+ }
6722
+ this.logger.info(`Scanning ${files.length} file(s) in ${label}`);
6723
+ for (const filePath of files) {
6724
+ let composition;
6725
+ try {
6726
+ composition = await this.fileSystem.readFile(filePath);
6727
+ } catch {
6728
+ this.logger.warn(`Could not read file: ${filePath} \u2014 skipping`);
6729
+ continue;
6730
+ }
6731
+ if (!composition?.composition) continue;
6732
+ this.collectTypesFromTree(composition.composition, usedTypeIds);
6733
+ }
6734
+ }
6735
+ collectTypesFromTree(node, usedTypeIds) {
6736
+ if (node.type) {
6737
+ usedTypeIds.add(node.type);
6738
+ }
6739
+ if (node.slots) {
6740
+ for (const slotInstances of Object.values(node.slots)) {
6741
+ if (!Array.isArray(slotInstances)) continue;
6742
+ for (const instance of slotInstances) {
6743
+ if (instance && typeof instance === "object") {
6744
+ this.collectTypesFromTree(
6745
+ instance,
6746
+ usedTypeIds
6747
+ );
6748
+ }
6749
+ }
6750
+ }
6751
+ }
6752
+ }
6753
+ async collectAllowedComponents(componentFiles, usedTypeIds) {
6754
+ for (const filePath of componentFiles) {
6755
+ let comp;
6756
+ try {
6757
+ comp = await this.fileSystem.readFile(filePath);
6758
+ } catch {
6759
+ continue;
6760
+ }
6761
+ if (!comp?.slots) continue;
6762
+ for (const slot of comp.slots) {
6763
+ if (!slot.allowedComponents) continue;
6764
+ for (const allowedType of slot.allowedComponents) {
6765
+ usedTypeIds.add(allowedType);
6766
+ }
6767
+ }
6768
+ }
6769
+ }
6770
+ isUsed(componentId, usedTypeIds, strict) {
6771
+ if (strict) {
6772
+ return usedTypeIds.has(componentId);
6773
+ }
6774
+ const lower = componentId.toLowerCase();
6775
+ for (const used of usedTypeIds) {
6776
+ if (used.toLowerCase() === lower) return true;
6777
+ }
6778
+ return false;
6779
+ }
6780
+ };
6781
+
6782
+ // src/cli/commands/remove-unused-component-types.ts
6783
+ function createRemoveUnusedComponentTypesCommand() {
6784
+ const command = new Command18("remove-unused-component-types");
6785
+ command.description(
6786
+ "Removes component definition files that are not referenced in any composition, pattern, or allowedComponents list."
6787
+ ).option(
6788
+ "--excludeComponentTypes <types>",
6789
+ 'Pipe-separated list of component type IDs to keep even if unused (e.g. "Hero|Button")'
6790
+ ).action(async (_opts, cmd) => {
6791
+ const globalOpts = cmd.optsWithGlobals();
6792
+ const options = {
6793
+ ...globalOpts
6794
+ };
6795
+ const logger = new Logger();
6796
+ logger.info(`rootDir: ${options.rootDir}`);
6797
+ logger.info(`componentsDir: ${options.componentsDir}`);
6798
+ logger.info(`compositionsDir: ${options.compositionsDir}`);
6799
+ logger.info(`compositionPatternsDir: ${options.compositionPatternsDir}`);
6800
+ logger.info(`componentPatternsDir: ${options.componentPatternsDir}`);
6801
+ logger.info(`excludeComponentTypes: ${options.excludeComponentTypes ?? ""}`);
6802
+ const excludeComponentTypes = options.excludeComponentTypes ? splitList(options.excludeComponentTypes) : [];
6803
+ const fileSystem = new FileSystemService();
6804
+ const service = new UnusedComponentTypeRemoverService(fileSystem, logger);
6805
+ const result = await service.remove({
6806
+ rootDir: options.rootDir,
6807
+ componentsDir: options.componentsDir,
6808
+ compositionsDir: options.compositionsDir,
6809
+ compositionPatternsDir: options.compositionPatternsDir,
6810
+ componentPatternsDir: options.componentPatternsDir,
6811
+ whatIf: options.whatIf ?? false,
6812
+ strict: options.strict ?? false,
6813
+ excludeComponentTypes
6814
+ });
6815
+ logger.success(
6816
+ `Removed ${result.removedComponentTypes} unused component type(s). ${result.retainedComponentTypes} component type(s) retained.`
6817
+ );
6818
+ });
6819
+ return command;
6820
+ }
6821
+
6822
+ // src/cli/commands/generate-missing-project-map-nodes.ts
6823
+ import { Command as Command19 } from "commander";
6824
+
6825
+ // src/core/services/generate-missing-project-map-nodes.service.ts
6826
+ var GenerateMissingProjectMapNodesService = class {
6827
+ constructor(fileSystem, logger) {
6828
+ this.fileSystem = fileSystem;
6829
+ this.logger = logger;
6830
+ }
6831
+ async generate(options) {
6832
+ const { rootDir, projectMapNodesDir, compositionsDir, rootContentTypes, whatIf, strict } = options;
6833
+ const nodesDirFull = this.fileSystem.resolvePath(rootDir, projectMapNodesDir);
6834
+ const nodesDirExists = await this.fileSystem.fileExists(nodesDirFull);
6835
+ if (!nodesDirExists) {
6836
+ this.logger.warn(`Project map nodes directory not found: ${projectMapNodesDir} \u2014 nothing to do`);
6837
+ return { generatedCount: 0, alreadyCoveredCount: 0 };
6838
+ }
6839
+ const nodeFiles = await this.fileSystem.findFiles(nodesDirFull, "**/*.{json,yaml,yml}");
6840
+ if (nodeFiles.length === 0) {
6841
+ this.logger.warn("No project map node files found \u2014 nothing to do");
6842
+ return { generatedCount: 0, alreadyCoveredCount: 0 };
6843
+ }
6844
+ this.logger.info(`Loaded ${nodeFiles.length} project map node(s)`);
6845
+ const allNodes = [];
6846
+ for (const filePath of nodeFiles) {
6847
+ let node;
6848
+ try {
6849
+ node = await this.fileSystem.readFile(filePath);
6850
+ } catch {
6851
+ this.logger.warn(`Could not read project map node file: ${filePath} \u2014 skipping`);
6852
+ continue;
6853
+ }
6854
+ if (!node?.path) {
6855
+ this.logger.warn(`Project map node missing path field: ${filePath} \u2014 skipping`);
6856
+ continue;
6857
+ }
6858
+ allNodes.push({ filePath, node });
6859
+ }
6860
+ const existingPaths = new Set(allNodes.map(({ node }) => node.path));
6861
+ const compositionsDirFull = this.fileSystem.resolvePath(rootDir, compositionsDir);
6862
+ const compositionIdToType = /* @__PURE__ */ new Map();
6863
+ const compositionsDirExists = await this.fileSystem.fileExists(compositionsDirFull);
6864
+ if (!compositionsDirExists) {
6865
+ this.logger.warn(`Compositions directory not found: ${compositionsDir} \u2014 no source nodes will match`);
6866
+ } else {
6867
+ const compositionFiles = await this.fileSystem.findFiles(
6868
+ compositionsDirFull,
6869
+ "**/*.{json,yaml,yml}"
6870
+ );
6871
+ for (const filePath of compositionFiles) {
6872
+ let comp;
6873
+ try {
6874
+ comp = await this.fileSystem.readFile(filePath);
6875
+ } catch {
6876
+ this.logger.warn(`Could not read composition file: ${filePath} \u2014 skipping`);
6877
+ continue;
6878
+ }
6879
+ if (comp?.composition?._id && comp?.composition?.type) {
6880
+ compositionIdToType.set(comp.composition._id, comp.composition.type);
6881
+ }
6882
+ }
6883
+ }
6884
+ const sourceNodes = allNodes.filter(({ node }) => {
6885
+ if (!node.compositionId) return false;
6886
+ const rootType = compositionIdToType.get(node.compositionId);
6887
+ if (!rootType) {
6888
+ this.logger.warn(
6889
+ `No composition found for compositionId "${node.compositionId}" (node path: ${node.path}) \u2014 skipping`
6890
+ );
6891
+ return false;
6892
+ }
6893
+ return this.matchesContentType(rootType, rootContentTypes, strict);
6894
+ });
6895
+ this.logger.info(`${sourceNodes.length} node(s) match root content types`);
6896
+ const ancestorPaths = /* @__PURE__ */ new Set();
6897
+ for (const { node } of sourceNodes) {
6898
+ for (const ancestor of this.computeAncestorPaths(node.path)) {
6899
+ ancestorPaths.add(ancestor);
6900
+ }
6901
+ }
6902
+ const missingPaths = [];
6903
+ let alreadyCoveredCount = 0;
6904
+ for (const ancestorPath of ancestorPaths) {
6905
+ if (existingPaths.has(ancestorPath)) {
6906
+ alreadyCoveredCount++;
6907
+ } else {
6908
+ missingPaths.push(ancestorPath);
6909
+ }
6910
+ }
6911
+ this.logger.info(
6912
+ `Collected ${ancestorPaths.size} unique ancestor path(s), ${alreadyCoveredCount} already covered`
6913
+ );
6914
+ let generatedCount = 0;
6915
+ for (const missingPath of missingPaths) {
6916
+ const fileName = this.pathToFileName(missingPath);
6917
+ const nodeId = this.pathToNodeId(missingPath);
6918
+ const destFilePath = this.fileSystem.resolvePath(nodesDirFull, fileName);
6919
+ const relPath = this.fileSystem.joinPath(projectMapNodesDir, fileName);
6920
+ this.logger.action(whatIf, "CREATE", `${relPath} (path: ${missingPath}, type: group)`);
6921
+ if (!whatIf) {
6922
+ const newNode = {
6923
+ id: nodeId,
6924
+ path: missingPath,
6925
+ type: "group"
6926
+ };
6927
+ await this.fileSystem.writeFile(destFilePath, newNode);
6928
+ }
6929
+ generatedCount++;
6930
+ }
6931
+ return { generatedCount, alreadyCoveredCount };
6932
+ }
6933
+ computeAncestorPaths(nodePath) {
6934
+ const segments = nodePath.split("/").filter((s) => s.length > 0);
6935
+ const ancestors = [];
6936
+ ancestors.push("/");
6937
+ for (let i = 1; i < segments.length; i++) {
6938
+ ancestors.push("/" + segments.slice(0, i).join("/"));
6939
+ }
6940
+ return ancestors;
6941
+ }
6942
+ pathToNodeId(nodePath) {
6943
+ if (nodePath === "/") return "pmn-group-root";
6944
+ const slug = nodePath.split("/").filter((s) => s.length > 0).join("-");
6945
+ return `pmn-group-${slug}`;
6946
+ }
6947
+ pathToFileName(nodePath) {
6948
+ if (nodePath === "/") return "root-node.yaml";
6949
+ const slug = nodePath.split("/").filter((s) => s.length > 0).join("-");
6950
+ return `${slug}-node.yaml`;
6951
+ }
6952
+ matchesContentType(rootType, rootContentTypes, strict) {
6953
+ for (const ct of rootContentTypes) {
6954
+ if (strict) {
6955
+ if (rootType === ct) return true;
6956
+ } else {
6957
+ if (rootType.toLowerCase() === ct.toLowerCase()) return true;
6958
+ }
6959
+ }
6960
+ return false;
6961
+ }
6962
+ };
6963
+
6964
+ // src/cli/commands/generate-missing-project-map-nodes.ts
6965
+ function createGenerateMissingProjectMapNodesCommand() {
6966
+ const command = new Command19("generate-missing-project-map-nodes");
6967
+ command.description(
6968
+ "Creates group project map nodes for ancestor path segments that are missing from the project map. Only paths reachable from compositions whose root type matches --rootContentTypes are considered."
6969
+ ).option(
6970
+ "--rootContentTypes <types>",
6971
+ 'Pipe-separated list of root component types identifying the leaf-page compositions whose ancestor paths should be filled in (e.g. "BlogPost|NewsArticle")'
6972
+ ).hook("preAction", (thisCommand) => {
6973
+ const opts = thisCommand.opts();
6974
+ const requiredOptions = [{ name: "rootContentTypes", flag: "--rootContentTypes" }];
6975
+ const missing = requiredOptions.filter((opt) => !opts[opt.name]).map((opt) => opt.flag);
6976
+ if (missing.length > 0) {
6977
+ console.error(`error: missing required options: ${missing.join(", ")}`);
6978
+ process.exit(1);
6979
+ }
6980
+ }).action(async (opts, cmd) => {
6981
+ const globalOpts = cmd.optsWithGlobals();
6982
+ const options = {
6983
+ ...globalOpts,
6984
+ rootContentTypes: opts.rootContentTypes
6985
+ };
6986
+ const logger = new Logger();
6987
+ logger.info(`rootContentTypes: ${options.rootContentTypes}`);
6988
+ const rootContentTypes = splitList(options.rootContentTypes);
6989
+ const fileSystem = new FileSystemService();
6990
+ const service = new GenerateMissingProjectMapNodesService(fileSystem, logger);
6991
+ const result = await service.generate({
6992
+ rootDir: options.rootDir,
6993
+ projectMapNodesDir: options.projectMapNodesDir,
6994
+ compositionsDir: options.compositionsDir,
6995
+ rootContentTypes,
6996
+ whatIf: options.whatIf ?? false,
6997
+ strict: options.strict ?? false
6998
+ });
6999
+ logger.success(
7000
+ `Created ${result.generatedCount} missing project map node(s). ${result.alreadyCoveredCount} ancestor path(s) already existed.`
7001
+ );
7002
+ });
7003
+ return command;
7004
+ }
7005
+
6497
7006
  // package.json
6498
7007
  var package_default = {
6499
7008
  name: "@uniformdev/transformer",
6500
- version: "1.1.39",
7009
+ version: "1.1.41",
6501
7010
  description: "CLI tool for transforming Uniform.dev serialization files offline",
6502
7011
  type: "module",
6503
7012
  bin: {
@@ -6566,7 +7075,7 @@ var package_default = {
6566
7075
  };
6567
7076
 
6568
7077
  // src/cli/index.ts
6569
- var program = new Command18();
7078
+ var program = new Command20();
6570
7079
  var appVersion = package_default.version;
6571
7080
  console.error(`uniform-transform v${appVersion}`);
6572
7081
  program.name("uniform-transform").description("CLI tool for transforming Uniform.dev serialization files offline").version(appVersion);
@@ -6579,6 +7088,9 @@ program.requiredOption("--rootDir <path>", "Path to the serialization project ro
6579
7088
  "Project map definitions directory name",
6580
7089
  "projectMapDefinition"
6581
7090
  ).option("--projectMapNodesDir <dir>", "Project map nodes directory name", "projectMapNode").option("--quirksDir <dir>", "Quirks directory name", "quirk");
7091
+ program.hook("preAction", (_thisCommand, actionCommand) => {
7092
+ console.error(`[INFO] ${actionCommand.name()} args: ${JSON.stringify(actionCommand.optsWithGlobals())}`);
7093
+ });
6582
7094
  program.addCommand(createPropagateRootComponentPropertyCommand());
6583
7095
  program.addCommand(createFindCompositionPatternCandidatesCommand());
6584
7096
  program.addCommand(createUnpackSerializationCommand());
@@ -6596,5 +7108,7 @@ program.addCommand(createAddContentTypeFieldCommand());
6596
7108
  program.addCommand(createFlattenBlockFieldCommand());
6597
7109
  program.addCommand(createRemoveOrphanEntriesCommand());
6598
7110
  program.addCommand(createRemoveUnusedContentTypesCommand());
7111
+ program.addCommand(createRemoveUnusedComponentTypesCommand());
7112
+ program.addCommand(createGenerateMissingProjectMapNodesCommand());
6599
7113
  program.parse();
6600
7114
  //# sourceMappingURL=index.js.map