@uniformdev/transformer 1.1.16 → 1.1.18

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
@@ -523,301 +523,793 @@ var CompositionService = class {
523
523
  }
524
524
  };
525
525
 
526
- // src/core/services/property-propagator.service.ts
527
- var PropertyPropagatorService = class {
526
+ // src/core/services/composition-converter.service.ts
527
+ import * as crypto from "crypto";
528
+ var CompositionConverterService = class {
528
529
  constructor(fileSystem, componentService, compositionService, logger) {
529
530
  this.fileSystem = fileSystem;
530
531
  this.componentService = componentService;
531
532
  this.compositionService = compositionService;
532
533
  this.logger = logger;
533
534
  }
534
- async propagate(options) {
535
+ async convert(options) {
535
536
  const {
536
537
  rootDir,
537
- componentsDir,
538
538
  compositionsDir,
539
- compositionType,
540
- property,
541
- targetComponentType,
542
- targetGroup,
539
+ componentsDir,
540
+ contentTypesDir,
541
+ entriesDir,
542
+ compositionTypes,
543
+ flattenComponentIds,
543
544
  whatIf,
544
- strict,
545
- deleteSourceParameter
545
+ strict
546
546
  } = options;
547
- const findOptions = { strict };
548
- const compositionTypes = this.parsePipeSeparatedValues(compositionType, strict);
549
- const fullComponentsDir = this.fileSystem.resolvePath(rootDir, componentsDir);
550
- const fullCompositionsDir = this.fileSystem.resolvePath(rootDir, compositionsDir);
551
- const sourceComponents = [];
552
- for (const sourceType of compositionTypes) {
553
- this.logger.info(`Loading component: ${sourceType}`);
554
- const { component: sourceComponent, filePath: sourceFilePath } = await this.componentService.loadComponent(fullComponentsDir, sourceType, findOptions);
555
- sourceComponents.push({ sourceType, sourceFilePath, sourceComponent });
547
+ const compositionsDirFull = this.fileSystem.resolvePath(rootDir, compositionsDir);
548
+ const componentsDirFull = this.fileSystem.resolvePath(rootDir, componentsDir);
549
+ const contentTypesDirFull = this.fileSystem.resolvePath(rootDir, contentTypesDir);
550
+ const entriesDirFull = this.fileSystem.resolvePath(rootDir, entriesDir);
551
+ let contentTypesWritten = 0;
552
+ let entriesFromCompositions = 0;
553
+ let entriesFromFlattened = 0;
554
+ let entriesReused = 0;
555
+ this.logger.info(`Composition types: ${compositionTypes.join(", ")}`);
556
+ if (flattenComponentIds.length > 0) {
557
+ this.logger.info(`Flatten component types: ${flattenComponentIds.join(", ")}`);
556
558
  }
557
- this.logger.info(`Loading component: ${targetComponentType}`);
558
- const { component: targetComponent, filePath: targetFilePath } = await this.componentService.loadComponent(fullComponentsDir, targetComponentType, findOptions);
559
- const propertyNames = property.split("|").map((p) => p.trim()).filter((p) => p.length > 0);
560
- const resolvedParams = [];
561
- const resolvedNames = [];
562
- for (const { sourceType, sourceComponent } of sourceComponents) {
563
- const { parameters: sourceParams, notFound } = this.componentService.resolveProperties(
564
- sourceComponent,
565
- propertyNames,
566
- findOptions
559
+ const sourceItemMap = flattenComponentIds.length > 0 ? await this.buildSourceItemMap(entriesDirFull) : /* @__PURE__ */ new Map();
560
+ if (sourceItemMap.size > 0) {
561
+ this.logger.info(`Found ${sourceItemMap.size} existing entry(ies) with sourceItem values`);
562
+ }
563
+ const compositionResults = await this.compositionService.findCompositionsByTypes(
564
+ compositionsDirFull,
565
+ compositionTypes,
566
+ { strict }
567
+ );
568
+ if (compositionResults.length === 0) {
569
+ this.logger.warn("No compositions found matching the specified types");
570
+ return { contentTypesWritten: 0, entriesFromCompositions: 0, entriesFromFlattened: 0, entriesReused: 0 };
571
+ }
572
+ this.logger.info(`Found ${compositionResults.length} composition(s)`);
573
+ const rootComponentTypes = /* @__PURE__ */ new Set();
574
+ for (const { composition } of compositionResults) {
575
+ rootComponentTypes.add(composition.composition.type);
576
+ }
577
+ const contentTypeMap = /* @__PURE__ */ new Map();
578
+ for (const rootType of rootComponentTypes) {
579
+ const { component } = await this.componentService.loadComponent(
580
+ componentsDirFull,
581
+ rootType,
582
+ { strict }
567
583
  );
568
- if (notFound.length > 0) {
569
- this.logger.warn(`Property "${notFound.join(", ")}" not found on component "${sourceType}"`);
584
+ const contentType = this.generateContentType(component);
585
+ contentTypeMap.set(rootType, contentType);
586
+ }
587
+ const flattenContentTypeMap = /* @__PURE__ */ new Map();
588
+ const missingFlattenTypes = [];
589
+ const foundMissingFlattenTypes = /* @__PURE__ */ new Set();
590
+ for (const flattenType of flattenComponentIds) {
591
+ const isRootType = [...rootComponentTypes].some(
592
+ (rt) => this.compareTypes(rt, flattenType, strict)
593
+ );
594
+ if (isRootType) {
570
595
  continue;
571
596
  }
572
- for (const param of sourceParams) {
573
- const exists = resolvedParams.some(
574
- (existing) => strict ? existing.id === param.id : existing.id.toLowerCase() === param.id.toLowerCase()
597
+ try {
598
+ const { component } = await this.componentService.loadComponent(
599
+ componentsDirFull,
600
+ flattenType,
601
+ { strict }
575
602
  );
576
- if (!exists) {
577
- resolvedParams.push(param);
578
- resolvedNames.push(param.id);
603
+ const contentType = this.generateContentType(component);
604
+ flattenContentTypeMap.set(flattenType, contentType);
605
+ } catch (error) {
606
+ if (error instanceof ComponentNotFoundError) {
607
+ this.logger.info(`Flatten component type not found: ${flattenType}`);
608
+ missingFlattenTypes.push(flattenType);
609
+ continue;
579
610
  }
611
+ throw error;
580
612
  }
581
613
  }
582
- const groupSources = [];
583
- for (const { sourceType, sourceComponent } of sourceComponents) {
584
- for (const name of propertyNames) {
585
- const param = this.componentService.findParameter(sourceComponent, name, findOptions);
586
- if (param && this.componentService.isGroupParameter(param)) {
587
- const groupSource = `${sourceType}.${name}`;
588
- if (!groupSources.includes(groupSource)) {
589
- groupSources.push(groupSource);
614
+ for (const { composition } of compositionResults) {
615
+ const comp = composition.composition;
616
+ const compositionId = comp._id;
617
+ const compositionName = comp._name ?? compositionId;
618
+ const compositionType = comp.type;
619
+ const entry = this.generateEntryFromComposition(composition);
620
+ const flattenedByType = /* @__PURE__ */ new Map();
621
+ if (flattenComponentIds.length > 0 && comp.slots) {
622
+ for (const flattenType of flattenComponentIds) {
623
+ if (this.compareTypes(flattenType, compositionType, strict)) {
624
+ this.logger.warn(
625
+ `Skipping flatten of "${flattenType}" \u2014 same as root component type`
626
+ );
627
+ continue;
590
628
  }
591
- }
592
- }
593
- }
594
- if (groupSources.length > 0) {
595
- this.logger.info(
596
- `Resolved properties: ${resolvedNames.join(", ")} (from ${groupSources.join(", ")})`
597
- );
598
- } else {
599
- this.logger.info(`Resolved properties: ${resolvedNames.join(", ")}`);
600
- }
601
- let modifiedComponent = { ...targetComponent };
602
- let componentModified = false;
603
- const groupId = this.componentService.generateGroupId(targetGroup);
604
- const existingGroup = this.componentService.findParameter(
605
- modifiedComponent,
606
- groupId,
607
- findOptions
608
- );
609
- if (!existingGroup) {
610
- this.logger.action(whatIf, "CREATE", `Group "${targetGroup}" on ${targetComponentType}`);
611
- modifiedComponent = this.componentService.ensureGroupExists(
612
- modifiedComponent,
613
- groupId,
614
- targetGroup,
615
- findOptions
616
- );
617
- componentModified = true;
618
- }
619
- for (const param of resolvedParams) {
620
- const existingParam = this.componentService.findParameter(
621
- modifiedComponent,
622
- param.id,
623
- findOptions
624
- );
625
- if (!existingParam) {
626
- this.logger.action(
627
- whatIf,
628
- "COPY",
629
- `Parameter "${param.id}" \u2192 ${targetComponentType}.${targetGroup}`
630
- );
631
- modifiedComponent = this.componentService.addParameterToComponent(
632
- modifiedComponent,
633
- param,
634
- findOptions
635
- );
636
- modifiedComponent = this.componentService.addParameterToGroup(
637
- modifiedComponent,
638
- groupId,
639
- param.id,
640
- findOptions
641
- );
642
- componentModified = true;
643
- } else {
644
- this.logger.info(`Parameter "${param.id}" already exists on ${targetComponentType}`);
645
- const group = this.componentService.findParameter(
646
- modifiedComponent,
647
- groupId,
648
- findOptions
649
- );
650
- if (group && this.componentService.isGroupParameter(group)) {
651
- const isInGroup = group.typeConfig?.childrenParams?.some(
652
- (id) => strict ? id === param.id : id.toLowerCase() === param.id.toLowerCase()
629
+ const instances = this.findFlattenTargets(
630
+ comp.slots,
631
+ flattenType,
632
+ compositionId,
633
+ compositionName,
634
+ strict
653
635
  );
654
- if (!isInGroup) {
655
- modifiedComponent = this.componentService.addParameterToGroup(
656
- modifiedComponent,
657
- groupId,
658
- param.id,
659
- findOptions
660
- );
661
- componentModified = true;
636
+ if (instances.length > 0) {
637
+ flattenedByType.set(flattenType, instances);
638
+ if (missingFlattenTypes.includes(flattenType)) {
639
+ foundMissingFlattenTypes.add(flattenType);
640
+ }
662
641
  }
663
642
  }
664
643
  }
665
- }
666
- if (componentModified && !whatIf) {
667
- await this.componentService.saveComponent(targetFilePath, modifiedComponent);
668
- }
669
- const compositions = await this.compositionService.findCompositionsByTypes(
670
- fullCompositionsDir,
671
- compositionTypes,
672
- findOptions
673
- );
674
- let modifiedCompositions = 0;
675
- let propagatedInstances = 0;
676
- for (const { composition, filePath } of compositions) {
677
- const rootOverrides = this.compositionService.getRootOverrides(composition);
678
- const instances = this.compositionService.findComponentInstances(
679
- composition,
680
- targetComponentType,
681
- findOptions
682
- );
683
- if (instances.length === 0) {
684
- continue;
685
- }
686
- const valuesToPropagate = {};
687
- for (const param of resolvedParams) {
688
- if (rootOverrides[param.id]) {
689
- valuesToPropagate[param.id] = rootOverrides[param.id];
644
+ const resolvedRefIds = /* @__PURE__ */ new Map();
645
+ for (const [flattenType, instances] of flattenedByType) {
646
+ const refIds = [];
647
+ for (const inst of instances) {
648
+ const existingId = this.findExistingEntryBySourceItem(inst, sourceItemMap);
649
+ refIds.push(existingId ?? inst.determinisiticId);
690
650
  }
651
+ resolvedRefIds.set(flattenType, refIds);
691
652
  }
692
- if (Object.keys(valuesToPropagate).length === 0) {
693
- continue;
653
+ for (const [flattenType] of flattenedByType) {
654
+ entry.entry.fields[flattenType] = {
655
+ type: "contentReference",
656
+ value: resolvedRefIds.get(flattenType)
657
+ };
694
658
  }
695
- let compositionModified = false;
696
- const relativePath = filePath.replace(fullCompositionsDir, "").replace(/^[/\\]/, "");
697
- const instanceUpdates = [];
698
- for (const { instance, instanceId } of instances) {
699
- const instanceName = instance._id ?? instanceId;
700
- this.compositionService.setInstanceParameters(instance, valuesToPropagate);
701
- compositionModified = true;
702
- propagatedInstances++;
703
- instanceUpdates.push(
704
- `${targetComponentType} "${instanceName}": ${Object.keys(valuesToPropagate).join(", ")}`
705
- );
659
+ if (flattenComponentIds.length > 0) {
660
+ this.transformContentReferences(entry);
706
661
  }
707
- if (compositionModified) {
708
- this.logger.action(whatIf, "UPDATE", `composition/${relativePath}`);
709
- for (const update of instanceUpdates) {
710
- this.logger.detail(`\u2192 ${update}`);
711
- }
712
- if (!whatIf) {
713
- await this.compositionService.saveComposition(filePath, composition);
714
- }
715
- modifiedCompositions++;
662
+ const entryId = entry.entry._id;
663
+ const entryFilePath = this.fileSystem.joinPath(entriesDirFull, `${entryId}.json`);
664
+ this.logger.action(
665
+ whatIf,
666
+ "WRITE",
667
+ `${entriesDir}/${entryId}.json (${compositionType}, "${this.truncate(compositionName, 50)}")`
668
+ );
669
+ if (!whatIf) {
670
+ await this.fileSystem.writeFile(entryFilePath, entry);
716
671
  }
717
- }
718
- let modifiedSourceComponents = 0;
719
- if (deleteSourceParameter) {
720
- for (const { sourceType, sourceFilePath, sourceComponent } of sourceComponents) {
721
- let modifiedSource = { ...sourceComponent };
722
- let sourceComponentModified = false;
723
- for (const param of resolvedParams) {
724
- const exists = this.componentService.findParameter(modifiedSource, param.id, findOptions);
725
- if (exists) {
726
- this.logger.action(whatIf, "DELETE", `Parameter "${param.id}" from ${sourceType}`);
727
- modifiedSource = this.componentService.removeParameter(modifiedSource, param.id, findOptions);
728
- sourceComponentModified = true;
672
+ entriesFromCompositions++;
673
+ for (const [flattenType, instances] of flattenedByType) {
674
+ for (const inst of instances) {
675
+ const existingId = this.findExistingEntryBySourceItem(inst, sourceItemMap);
676
+ if (existingId) {
677
+ this.logger.info(
678
+ `Reusing existing entry ${existingId} for ${flattenType} (sourceItem match)`
679
+ );
680
+ entriesReused++;
681
+ continue;
729
682
  }
730
- }
731
- const beforeGroupCount = modifiedSource.parameters?.filter(
732
- (p) => this.componentService.isGroupParameter(p)
733
- ).length ?? 0;
734
- modifiedSource = this.componentService.removeEmptyGroups(modifiedSource);
735
- const afterGroupCount = modifiedSource.parameters?.filter(
736
- (p) => this.componentService.isGroupParameter(p)
737
- ).length ?? 0;
738
- if (afterGroupCount < beforeGroupCount) {
739
- const removedCount = beforeGroupCount - afterGroupCount;
683
+ const flatEntry = this.generateEntryFromFlattenedInstance(inst);
684
+ const flatEntryPath = this.fileSystem.joinPath(
685
+ entriesDirFull,
686
+ `${inst.determinisiticId}.json`
687
+ );
740
688
  this.logger.action(
741
689
  whatIf,
742
- "DELETE",
743
- `${removedCount} empty group(s) from ${sourceType}`
690
+ "WRITE",
691
+ `${entriesDir}/${inst.determinisiticId}.json (${flattenType} from "${this.truncate(compositionName, 50)}")`
744
692
  );
745
- sourceComponentModified = true;
746
- }
747
- if (sourceComponentModified) {
748
- modifiedSourceComponents++;
749
- }
750
- if (sourceComponentModified && !whatIf) {
751
- await this.componentService.saveComponent(sourceFilePath, modifiedSource);
693
+ if (!whatIf) {
694
+ await this.fileSystem.writeFile(flatEntryPath, flatEntry);
695
+ }
696
+ entriesFromFlattened++;
752
697
  }
753
698
  }
754
- for (const { composition, filePath } of compositions) {
755
- const relativePath = filePath.replace(fullCompositionsDir, "").replace(/^[/\\]/, "");
756
- const deleted = this.compositionService.deleteRootOverrides(
757
- composition,
758
- resolvedNames,
759
- findOptions
760
- );
761
- if (deleted) {
762
- this.logger.action(whatIf, "DELETE", `Root overrides from composition/${relativePath}`);
763
- this.logger.detail(`\u2192 Removed: ${resolvedNames.join(", ")}`);
764
- if (!whatIf) {
765
- await this.compositionService.saveComposition(filePath, composition);
699
+ }
700
+ if (flattenComponentIds.length > 0) {
701
+ for (const contentType of contentTypeMap.values()) {
702
+ for (const flattenType of flattenComponentIds) {
703
+ if (this.compareTypes(flattenType, contentType.id, strict)) {
704
+ continue;
766
705
  }
706
+ if (missingFlattenTypes.includes(flattenType) && !foundMissingFlattenTypes.has(flattenType)) {
707
+ continue;
708
+ }
709
+ contentType.fields.push({
710
+ id: flattenType,
711
+ name: flattenType,
712
+ type: "contentReference",
713
+ typeConfig: {
714
+ isMulti: true,
715
+ allowedContentTypes: [flattenType]
716
+ },
717
+ localizable: false
718
+ });
767
719
  }
768
720
  }
769
721
  }
770
- return {
771
- modifiedComponents: (componentModified ? 1 : 0) + modifiedSourceComponents,
772
- modifiedCompositions,
773
- propagatedInstances
774
- };
775
- }
776
- parsePipeSeparatedValues(value, strict) {
777
- const entries = value.split("|").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
778
- const normalized = [];
779
- for (const entry of entries) {
780
- const exists = normalized.some(
781
- (existing) => strict ? existing === entry : existing.toLowerCase() === entry.toLowerCase()
722
+ for (const [typeName, contentType] of contentTypeMap) {
723
+ const filePath = this.fileSystem.joinPath(contentTypesDirFull, `${typeName}.json`);
724
+ const fieldCount = contentType.fields.filter((f) => f.type !== "contentReference").length;
725
+ const refCount = contentType.fields.filter((f) => f.type === "contentReference").length;
726
+ const refInfo = refCount > 0 ? ` + ${refCount} reference(s)` : "";
727
+ this.logger.action(
728
+ whatIf,
729
+ "WRITE",
730
+ `${contentTypesDir}/${typeName}.json (${fieldCount} fields${refInfo})`
782
731
  );
783
- if (!exists) {
784
- normalized.push(entry);
732
+ if (!whatIf) {
733
+ await this.fileSystem.writeFile(filePath, contentType);
785
734
  }
735
+ contentTypesWritten++;
786
736
  }
787
- return normalized;
788
- }
789
- };
790
-
791
- // src/cli/logger.ts
792
- import chalk from "chalk";
793
- var Logger = class {
794
- info(message) {
795
- console.log(`${chalk.blue("[INFO]")} ${message}`);
737
+ for (const [typeName, contentType] of flattenContentTypeMap) {
738
+ const filePath = this.fileSystem.joinPath(contentTypesDirFull, `${typeName}.json`);
739
+ this.logger.action(
740
+ whatIf,
741
+ "WRITE",
742
+ `${contentTypesDir}/${typeName}.json (${contentType.fields.length} fields)`
743
+ );
744
+ if (!whatIf) {
745
+ await this.fileSystem.writeFile(filePath, contentType);
746
+ }
747
+ contentTypesWritten++;
748
+ }
749
+ const neverFoundMissingTypes = missingFlattenTypes.filter(
750
+ (type) => !foundMissingFlattenTypes.has(type)
751
+ );
752
+ if (neverFoundMissingTypes.length > 0) {
753
+ this.logger.warn(
754
+ `Flatten component type(s) not found in any composition: ${neverFoundMissingTypes.join(", ")}`
755
+ );
756
+ }
757
+ return { contentTypesWritten, entriesFromCompositions, entriesFromFlattened, entriesReused };
796
758
  }
797
- success(message) {
798
- console.log(`${chalk.green("[DONE]")} ${message}`);
759
+ // --- Content Type Generation ---
760
+ generateContentType(component) {
761
+ const fields = [];
762
+ if (component.parameters) {
763
+ for (const param of component.parameters) {
764
+ fields.push(this.parameterToField(param));
765
+ }
766
+ }
767
+ return {
768
+ id: component.id,
769
+ name: component.name,
770
+ fields
771
+ };
799
772
  }
800
- error(message) {
801
- console.log(`${chalk.red("[ERROR]")} ${message}`);
773
+ parameterToField(param) {
774
+ const field = {
775
+ id: param.id,
776
+ name: param.name,
777
+ type: param.type
778
+ };
779
+ if (param.helpText !== void 0) {
780
+ field.helpText = param.helpText;
781
+ }
782
+ if (param.localizable !== void 0) {
783
+ field.localizable = param.localizable;
784
+ }
785
+ if (param.typeConfig !== void 0) {
786
+ field.typeConfig = param.typeConfig;
787
+ }
788
+ return field;
802
789
  }
803
- warn(message) {
804
- console.log(`${chalk.yellow("[WARN]")} ${message}`);
790
+ // --- Entry Generation ---
791
+ generateEntryFromComposition(composition) {
792
+ const comp = composition.composition;
793
+ const compositionSpecificKeys = /* @__PURE__ */ new Set(["_id", "_name", "type", "parameters", "slots", "_overrides"]);
794
+ const extraRootProps = {};
795
+ for (const [key, value] of Object.entries(comp)) {
796
+ if (!compositionSpecificKeys.has(key) && value != null) {
797
+ extraRootProps[key] = value;
798
+ }
799
+ }
800
+ const wrapperKeys = /* @__PURE__ */ new Set(["composition"]);
801
+ const extraWrapperProps = {};
802
+ for (const [key, value] of Object.entries(composition)) {
803
+ if (!wrapperKeys.has(key) && value != null) {
804
+ extraWrapperProps[key] = value;
805
+ }
806
+ }
807
+ const entryId = computeGuidHash(`entry${comp._id}`);
808
+ const entryName = this.truncateName(comp._name ?? comp._id, 60);
809
+ return {
810
+ entry: {
811
+ _id: entryId,
812
+ _name: entryName,
813
+ type: comp.type,
814
+ fields: { ...comp.parameters ?? {} },
815
+ ...extraRootProps
816
+ },
817
+ ...extraWrapperProps
818
+ };
805
819
  }
806
- action(whatIf, actionType, message) {
807
- const prefix = whatIf ? chalk.yellow("[WOULD]") : chalk.green(`[${actionType}]`);
808
- console.log(`${prefix} ${message}`);
820
+ generateEntryFromFlattenedInstance(inst) {
821
+ const entryName = this.truncateName(
822
+ `${inst.componentType} (from ${inst.compositionName})`,
823
+ 60
824
+ );
825
+ return {
826
+ entry: {
827
+ _id: inst.determinisiticId,
828
+ _name: entryName,
829
+ type: inst.componentType,
830
+ fields: { ...inst.instance.parameters ?? {} }
831
+ }
832
+ };
809
833
  }
810
- detail(message) {
811
- console.log(` ${chalk.gray(message)}`);
834
+ // --- Flatten Tree Walking ---
835
+ findFlattenTargets(slots, targetType, compositionId, compositionName, strict) {
836
+ const results = [];
837
+ this.walkSlots(slots, targetType, compositionId, compositionName, "", results, strict);
838
+ return results;
812
839
  }
813
- };
814
-
815
- // src/cli/commands/propagate-root-component-property.ts
816
- function createPropagateRootComponentPropertyCommand() {
817
- const command = new Command("propagate-root-component-property");
818
- command.description(
819
- "Copies property definitions from a composition type's root component to a target component type, then propagates the actual values across all matching compositions."
820
- ).option(
840
+ walkSlots(slots, targetType, compositionId, compositionName, pathPrefix, results, strict) {
841
+ for (const [slotName, instances] of Object.entries(slots)) {
842
+ if (!Array.isArray(instances)) continue;
843
+ for (let i = 0; i < instances.length; i++) {
844
+ const instance = instances[i];
845
+ if (instance._pattern) {
846
+ continue;
847
+ }
848
+ const currentPath = pathPrefix ? `${pathPrefix}-${slotName}-[${i}]-${instance.type}` : `${slotName}-[${i}]-${instance.type}`;
849
+ if (this.compareTypes(instance.type, targetType, strict)) {
850
+ const fullPath = `${compositionId}-${currentPath}`;
851
+ const deterministicId = computeGuidHash(fullPath);
852
+ results.push({
853
+ instance,
854
+ path: fullPath,
855
+ determinisiticId: deterministicId,
856
+ componentType: instance.type,
857
+ compositionId,
858
+ compositionName
859
+ });
860
+ }
861
+ if (instance.slots) {
862
+ this.walkSlots(
863
+ instance.slots,
864
+ targetType,
865
+ compositionId,
866
+ compositionName,
867
+ currentPath,
868
+ results,
869
+ strict
870
+ );
871
+ }
872
+ }
873
+ }
874
+ }
875
+ // --- Content Reference Transformation ---
876
+ transformContentReferences(entry) {
877
+ const dataResources = {};
878
+ for (const [fieldName, field] of Object.entries(entry.entry.fields)) {
879
+ if (field.type === "contentReference" && Array.isArray(field.value)) {
880
+ const entryIds = field.value;
881
+ const resourceKey = `ref-${entry.entry._id}-${fieldName}`;
882
+ field.value = `\${#jptr:/${resourceKey}/entries}`;
883
+ dataResources[resourceKey] = {
884
+ type: "uniformContentInternalReference",
885
+ variables: {
886
+ locale: "${locale}",
887
+ entryIds: entryIds.join(",")
888
+ }
889
+ };
890
+ }
891
+ }
892
+ if (Object.keys(dataResources).length > 0) {
893
+ entry.entry._dataResources = {
894
+ ...entry.entry._dataResources ?? {},
895
+ ...dataResources
896
+ };
897
+ }
898
+ }
899
+ // --- Source Item Matching ---
900
+ async buildSourceItemMap(entriesDirFull) {
901
+ const sourceItemMap = /* @__PURE__ */ new Map();
902
+ const entryFiles = await this.fileSystem.findFiles(entriesDirFull, "*.json");
903
+ for (const filePath of entryFiles) {
904
+ try {
905
+ const entryData = await this.fileSystem.readFile(filePath);
906
+ const sourceItemField = entryData?.entry?.fields?.sourceItem;
907
+ if (sourceItemField?.value != null) {
908
+ sourceItemMap.set(String(sourceItemField.value), entryData.entry._id);
909
+ }
910
+ } catch {
911
+ continue;
912
+ }
913
+ }
914
+ return sourceItemMap;
915
+ }
916
+ findExistingEntryBySourceItem(inst, sourceItemMap) {
917
+ const sourceItemParam = inst.instance.parameters?.sourceItem;
918
+ if (sourceItemParam?.value == null) {
919
+ return void 0;
920
+ }
921
+ return sourceItemMap.get(String(sourceItemParam.value));
922
+ }
923
+ // --- Utilities ---
924
+ compareTypes(type1, type2, strict) {
925
+ if (strict) {
926
+ return type1 === type2;
927
+ }
928
+ return type1.toLowerCase() === type2.toLowerCase();
929
+ }
930
+ truncate(str, maxLength) {
931
+ if (str.length <= maxLength) return str;
932
+ return str.substring(0, maxLength - 3) + "...";
933
+ }
934
+ truncateName(name, maxLength) {
935
+ if (name.length <= maxLength) return name;
936
+ return name.substring(0, maxLength);
937
+ }
938
+ };
939
+ function computeGuidHash(guidOrSeed) {
940
+ let uuidStr;
941
+ if (isValidUuid(guidOrSeed)) {
942
+ uuidStr = formatAsBracedUuid(guidOrSeed);
943
+ } else {
944
+ const hash = crypto.createHash("md5").update(guidOrSeed, "utf-8").digest();
945
+ const hex = [
946
+ // First 4 bytes: little-endian in .NET Guid
947
+ hash[3].toString(16).padStart(2, "0"),
948
+ hash[2].toString(16).padStart(2, "0"),
949
+ hash[1].toString(16).padStart(2, "0"),
950
+ hash[0].toString(16).padStart(2, "0"),
951
+ "-",
952
+ // Bytes 4-5: little-endian
953
+ hash[5].toString(16).padStart(2, "0"),
954
+ hash[4].toString(16).padStart(2, "0"),
955
+ "-",
956
+ // Bytes 6-7: little-endian
957
+ hash[7].toString(16).padStart(2, "0"),
958
+ hash[6].toString(16).padStart(2, "0"),
959
+ "-",
960
+ // Bytes 8-9: big-endian
961
+ hash[8].toString(16).padStart(2, "0"),
962
+ hash[9].toString(16).padStart(2, "0"),
963
+ "-",
964
+ // Bytes 10-15: big-endian
965
+ hash[10].toString(16).padStart(2, "0"),
966
+ hash[11].toString(16).padStart(2, "0"),
967
+ hash[12].toString(16).padStart(2, "0"),
968
+ hash[13].toString(16).padStart(2, "0"),
969
+ hash[14].toString(16).padStart(2, "0"),
970
+ hash[15].toString(16).padStart(2, "0")
971
+ ].join("");
972
+ uuidStr = `{${hex}}`.toUpperCase();
973
+ }
974
+ const chars = uuidStr.split("");
975
+ chars[15] = "4";
976
+ const arr20 = ["8", "9", "A", "B"];
977
+ const hexVal = parseInt(chars[20], 16);
978
+ chars[20] = arr20[hexVal % 4];
979
+ return chars.join("").slice(1, -1).toLowerCase();
980
+ }
981
+ function isValidUuid(str) {
982
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(str);
983
+ }
984
+ function formatAsBracedUuid(uuid) {
985
+ const clean = uuid.replace(/[{}]/g, "").toUpperCase();
986
+ return `{${clean}}`;
987
+ }
988
+
989
+ // src/core/services/id-regenerator.ts
990
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
991
+ function regenerateIds(value, basePath) {
992
+ return walkAndRegenerate(value, basePath);
993
+ }
994
+ function walkAndRegenerate(value, currentPath) {
995
+ if (value === null || value === void 0) {
996
+ return value;
997
+ }
998
+ if (Array.isArray(value)) {
999
+ return value.map((item, index) => walkAndRegenerate(item, `${currentPath}[${index}]`));
1000
+ }
1001
+ if (typeof value === "object") {
1002
+ const obj = value;
1003
+ const result = {};
1004
+ for (const [key, val] of Object.entries(obj)) {
1005
+ const childPath = `${currentPath}.${key}`;
1006
+ if (key === "_id" && typeof val === "string" && UUID_PATTERN.test(val)) {
1007
+ result[key] = computeGuidHash(val + childPath);
1008
+ } else {
1009
+ result[key] = walkAndRegenerate(val, childPath);
1010
+ }
1011
+ }
1012
+ return result;
1013
+ }
1014
+ return value;
1015
+ }
1016
+
1017
+ // src/core/services/property-propagator.service.ts
1018
+ var PropertyPropagatorService = class {
1019
+ constructor(fileSystem, componentService, compositionService, logger) {
1020
+ this.fileSystem = fileSystem;
1021
+ this.componentService = componentService;
1022
+ this.compositionService = compositionService;
1023
+ this.logger = logger;
1024
+ }
1025
+ async propagate(options) {
1026
+ const {
1027
+ rootDir,
1028
+ componentsDir,
1029
+ compositionsDir,
1030
+ compositionType,
1031
+ property,
1032
+ targetComponentType,
1033
+ targetGroup,
1034
+ whatIf,
1035
+ strict,
1036
+ deleteSourceParameter
1037
+ } = options;
1038
+ const findOptions = { strict };
1039
+ const compositionTypes = this.parsePipeSeparatedValues(compositionType, strict);
1040
+ const fullComponentsDir = this.fileSystem.resolvePath(rootDir, componentsDir);
1041
+ const fullCompositionsDir = this.fileSystem.resolvePath(rootDir, compositionsDir);
1042
+ const sourceComponents = [];
1043
+ for (const sourceType of compositionTypes) {
1044
+ this.logger.info(`Loading component: ${sourceType}`);
1045
+ const { component: sourceComponent, filePath: sourceFilePath } = await this.componentService.loadComponent(fullComponentsDir, sourceType, findOptions);
1046
+ sourceComponents.push({ sourceType, sourceFilePath, sourceComponent });
1047
+ }
1048
+ this.logger.info(`Loading component: ${targetComponentType}`);
1049
+ const { component: targetComponent, filePath: targetFilePath } = await this.componentService.loadComponent(fullComponentsDir, targetComponentType, findOptions);
1050
+ const propertyNames = property.split("|").map((p) => p.trim()).filter((p) => p.length > 0);
1051
+ const resolvedParams = [];
1052
+ const resolvedNames = [];
1053
+ for (const { sourceType, sourceComponent } of sourceComponents) {
1054
+ const { parameters: sourceParams, notFound } = this.componentService.resolveProperties(
1055
+ sourceComponent,
1056
+ propertyNames,
1057
+ findOptions
1058
+ );
1059
+ if (notFound.length > 0) {
1060
+ this.logger.warn(`Property "${notFound.join(", ")}" not found on component "${sourceType}"`);
1061
+ continue;
1062
+ }
1063
+ for (const param of sourceParams) {
1064
+ const exists = resolvedParams.some(
1065
+ (existing) => strict ? existing.id === param.id : existing.id.toLowerCase() === param.id.toLowerCase()
1066
+ );
1067
+ if (!exists) {
1068
+ resolvedParams.push(param);
1069
+ resolvedNames.push(param.id);
1070
+ }
1071
+ }
1072
+ }
1073
+ const groupSources = [];
1074
+ for (const { sourceType, sourceComponent } of sourceComponents) {
1075
+ for (const name of propertyNames) {
1076
+ const param = this.componentService.findParameter(sourceComponent, name, findOptions);
1077
+ if (param && this.componentService.isGroupParameter(param)) {
1078
+ const groupSource = `${sourceType}.${name}`;
1079
+ if (!groupSources.includes(groupSource)) {
1080
+ groupSources.push(groupSource);
1081
+ }
1082
+ }
1083
+ }
1084
+ }
1085
+ if (groupSources.length > 0) {
1086
+ this.logger.info(
1087
+ `Resolved properties: ${resolvedNames.join(", ")} (from ${groupSources.join(", ")})`
1088
+ );
1089
+ } else {
1090
+ this.logger.info(`Resolved properties: ${resolvedNames.join(", ")}`);
1091
+ }
1092
+ let modifiedComponent = { ...targetComponent };
1093
+ let componentModified = false;
1094
+ const groupId = this.componentService.generateGroupId(targetGroup);
1095
+ const existingGroup = this.componentService.findParameter(
1096
+ modifiedComponent,
1097
+ groupId,
1098
+ findOptions
1099
+ );
1100
+ if (!existingGroup) {
1101
+ this.logger.action(whatIf, "CREATE", `Group "${targetGroup}" on ${targetComponentType}`);
1102
+ modifiedComponent = this.componentService.ensureGroupExists(
1103
+ modifiedComponent,
1104
+ groupId,
1105
+ targetGroup,
1106
+ findOptions
1107
+ );
1108
+ componentModified = true;
1109
+ }
1110
+ for (const param of resolvedParams) {
1111
+ const existingParam = this.componentService.findParameter(
1112
+ modifiedComponent,
1113
+ param.id,
1114
+ findOptions
1115
+ );
1116
+ if (!existingParam) {
1117
+ this.logger.action(
1118
+ whatIf,
1119
+ "COPY",
1120
+ `Parameter "${param.id}" \u2192 ${targetComponentType}.${targetGroup}`
1121
+ );
1122
+ modifiedComponent = this.componentService.addParameterToComponent(
1123
+ modifiedComponent,
1124
+ param,
1125
+ findOptions
1126
+ );
1127
+ modifiedComponent = this.componentService.addParameterToGroup(
1128
+ modifiedComponent,
1129
+ groupId,
1130
+ param.id,
1131
+ findOptions
1132
+ );
1133
+ componentModified = true;
1134
+ } else {
1135
+ this.logger.info(`Parameter "${param.id}" already exists on ${targetComponentType}`);
1136
+ const group = this.componentService.findParameter(
1137
+ modifiedComponent,
1138
+ groupId,
1139
+ findOptions
1140
+ );
1141
+ if (group && this.componentService.isGroupParameter(group)) {
1142
+ const isInGroup = group.typeConfig?.childrenParams?.some(
1143
+ (id) => strict ? id === param.id : id.toLowerCase() === param.id.toLowerCase()
1144
+ );
1145
+ if (!isInGroup) {
1146
+ modifiedComponent = this.componentService.addParameterToGroup(
1147
+ modifiedComponent,
1148
+ groupId,
1149
+ param.id,
1150
+ findOptions
1151
+ );
1152
+ componentModified = true;
1153
+ }
1154
+ }
1155
+ }
1156
+ }
1157
+ if (componentModified && !whatIf) {
1158
+ await this.componentService.saveComponent(targetFilePath, modifiedComponent);
1159
+ }
1160
+ const compositions = await this.compositionService.findCompositionsByTypes(
1161
+ fullCompositionsDir,
1162
+ compositionTypes,
1163
+ findOptions
1164
+ );
1165
+ let modifiedCompositions = 0;
1166
+ let propagatedInstances = 0;
1167
+ for (const { composition, filePath } of compositions) {
1168
+ const rootOverrides = this.compositionService.getRootOverrides(composition);
1169
+ const instances = this.compositionService.findComponentInstances(
1170
+ composition,
1171
+ targetComponentType,
1172
+ findOptions
1173
+ );
1174
+ if (instances.length === 0) {
1175
+ continue;
1176
+ }
1177
+ const valuesToPropagate = {};
1178
+ for (const param of resolvedParams) {
1179
+ if (rootOverrides[param.id]) {
1180
+ valuesToPropagate[param.id] = rootOverrides[param.id];
1181
+ }
1182
+ }
1183
+ if (Object.keys(valuesToPropagate).length === 0) {
1184
+ continue;
1185
+ }
1186
+ let compositionModified = false;
1187
+ const relativePath = filePath.replace(fullCompositionsDir, "").replace(/^[/\\]/, "");
1188
+ const instanceUpdates = [];
1189
+ for (const { instance, instanceId } of instances) {
1190
+ const instanceName = instance._id ?? instanceId;
1191
+ const clonedValues = regenerateIds(valuesToPropagate, instanceName);
1192
+ this.compositionService.setInstanceParameters(instance, clonedValues);
1193
+ compositionModified = true;
1194
+ propagatedInstances++;
1195
+ instanceUpdates.push(
1196
+ `${targetComponentType} "${instanceName}": ${Object.keys(valuesToPropagate).join(", ")}`
1197
+ );
1198
+ }
1199
+ if (compositionModified) {
1200
+ this.logger.action(whatIf, "UPDATE", `composition/${relativePath}`);
1201
+ for (const update of instanceUpdates) {
1202
+ this.logger.detail(`\u2192 ${update}`);
1203
+ }
1204
+ if (!whatIf) {
1205
+ await this.compositionService.saveComposition(filePath, composition);
1206
+ }
1207
+ modifiedCompositions++;
1208
+ }
1209
+ }
1210
+ let modifiedSourceComponents = 0;
1211
+ if (deleteSourceParameter) {
1212
+ for (const { sourceType, sourceFilePath, sourceComponent } of sourceComponents) {
1213
+ let modifiedSource = { ...sourceComponent };
1214
+ let sourceComponentModified = false;
1215
+ for (const param of resolvedParams) {
1216
+ const exists = this.componentService.findParameter(modifiedSource, param.id, findOptions);
1217
+ if (exists) {
1218
+ this.logger.action(whatIf, "DELETE", `Parameter "${param.id}" from ${sourceType}`);
1219
+ modifiedSource = this.componentService.removeParameter(modifiedSource, param.id, findOptions);
1220
+ sourceComponentModified = true;
1221
+ }
1222
+ }
1223
+ const beforeGroupCount = modifiedSource.parameters?.filter(
1224
+ (p) => this.componentService.isGroupParameter(p)
1225
+ ).length ?? 0;
1226
+ modifiedSource = this.componentService.removeEmptyGroups(modifiedSource);
1227
+ const afterGroupCount = modifiedSource.parameters?.filter(
1228
+ (p) => this.componentService.isGroupParameter(p)
1229
+ ).length ?? 0;
1230
+ if (afterGroupCount < beforeGroupCount) {
1231
+ const removedCount = beforeGroupCount - afterGroupCount;
1232
+ this.logger.action(
1233
+ whatIf,
1234
+ "DELETE",
1235
+ `${removedCount} empty group(s) from ${sourceType}`
1236
+ );
1237
+ sourceComponentModified = true;
1238
+ }
1239
+ if (sourceComponentModified) {
1240
+ modifiedSourceComponents++;
1241
+ }
1242
+ if (sourceComponentModified && !whatIf) {
1243
+ await this.componentService.saveComponent(sourceFilePath, modifiedSource);
1244
+ }
1245
+ }
1246
+ for (const { composition, filePath } of compositions) {
1247
+ const relativePath = filePath.replace(fullCompositionsDir, "").replace(/^[/\\]/, "");
1248
+ const deleted = this.compositionService.deleteRootOverrides(
1249
+ composition,
1250
+ resolvedNames,
1251
+ findOptions
1252
+ );
1253
+ if (deleted) {
1254
+ this.logger.action(whatIf, "DELETE", `Root overrides from composition/${relativePath}`);
1255
+ this.logger.detail(`\u2192 Removed: ${resolvedNames.join(", ")}`);
1256
+ if (!whatIf) {
1257
+ await this.compositionService.saveComposition(filePath, composition);
1258
+ }
1259
+ }
1260
+ }
1261
+ }
1262
+ return {
1263
+ modifiedComponents: (componentModified ? 1 : 0) + modifiedSourceComponents,
1264
+ modifiedCompositions,
1265
+ propagatedInstances
1266
+ };
1267
+ }
1268
+ parsePipeSeparatedValues(value, strict) {
1269
+ const entries = value.split("|").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
1270
+ const normalized = [];
1271
+ for (const entry of entries) {
1272
+ const exists = normalized.some(
1273
+ (existing) => strict ? existing === entry : existing.toLowerCase() === entry.toLowerCase()
1274
+ );
1275
+ if (!exists) {
1276
+ normalized.push(entry);
1277
+ }
1278
+ }
1279
+ return normalized;
1280
+ }
1281
+ };
1282
+
1283
+ // src/cli/logger.ts
1284
+ import chalk from "chalk";
1285
+ var Logger = class {
1286
+ info(message) {
1287
+ console.log(`${chalk.blue("[INFO]")} ${message}`);
1288
+ }
1289
+ success(message) {
1290
+ console.log(`${chalk.green("[DONE]")} ${message}`);
1291
+ }
1292
+ error(message) {
1293
+ console.log(`${chalk.red("[ERROR]")} ${message}`);
1294
+ }
1295
+ warn(message) {
1296
+ console.log(`${chalk.yellow("[WARN]")} ${message}`);
1297
+ }
1298
+ action(whatIf, actionType, message) {
1299
+ const prefix = whatIf ? chalk.yellow("[WOULD]") : chalk.green(`[${actionType}]`);
1300
+ console.log(`${prefix} ${message}`);
1301
+ }
1302
+ detail(message) {
1303
+ console.log(` ${chalk.gray(message)}`);
1304
+ }
1305
+ };
1306
+
1307
+ // src/cli/commands/propagate-root-component-property.ts
1308
+ function createPropagateRootComponentPropertyCommand() {
1309
+ const command = new Command("propagate-root-component-property");
1310
+ command.description(
1311
+ "Copies property definitions from a composition type's root component to a target component type, then propagates the actual values across all matching compositions."
1312
+ ).option(
821
1313
  "--compositionType <type>",
822
1314
  "The composition type(s) to process. Supports pipe-separated lists (e.g., HomePage|LandingPage)"
823
1315
  ).option("--property <properties>", "Pipe-separated list of properties and/or groups to copy").option(
@@ -2807,9 +3299,23 @@ var ComponentAdderService = class {
2807
3299
  throw error;
2808
3300
  }
2809
3301
  let allowedComponentsUpdated = false;
3302
+ const resolvedParentTypes = [];
2810
3303
  for (const parentType of parentTypes) {
2811
- this.logger.info(`Loading parent component: ${parentType}`);
2812
- const { component: parentComponent, filePath: parentComponentFilePath } = await this.componentService.loadComponent(fullComponentsDir, parentType, findOptions);
3304
+ this.logger.info(`Loading component: ${parentType}`);
3305
+ let parentComponent;
3306
+ let parentComponentFilePath;
3307
+ try {
3308
+ const result = await this.componentService.loadComponent(fullComponentsDir, parentType, findOptions);
3309
+ parentComponent = result.component;
3310
+ parentComponentFilePath = result.filePath;
3311
+ } catch (error) {
3312
+ if (error instanceof ComponentNotFoundError) {
3313
+ this.logger.warn(`Component not found: ${parentType} (searched: ${fullComponentsDir})`);
3314
+ continue;
3315
+ }
3316
+ throw error;
3317
+ }
3318
+ resolvedParentTypes.push(parentType);
2813
3319
  if (!parentComponent.slots) {
2814
3320
  parentComponent.slots = [];
2815
3321
  }
@@ -2877,7 +3383,7 @@ var ComponentAdderService = class {
2877
3383
  );
2878
3384
  this.logger.info("");
2879
3385
  this.logger.info(
2880
- `Summary: ${allowedComponentsUpdated ? `${parentTypes.length} component definition(s) updated, ` : ""}${compositionsResult} composition(s), ${compositionPatternsResult} composition pattern(s), ${componentPatternsResult} component pattern(s) updated. ${compositionsResult + compositionPatternsResult + componentPatternsResult} instance(s) added.`
3386
+ `Summary: ${allowedComponentsUpdated ? `${resolvedParentTypes.length} component definition(s) updated, ` : ""}${compositionsResult} composition(s), ${compositionPatternsResult} composition pattern(s), ${componentPatternsResult} component pattern(s) updated. ${compositionsResult + compositionPatternsResult + componentPatternsResult} instance(s) added.`
2881
3387
  );
2882
3388
  return {
2883
3389
  allowedComponentsUpdated,
@@ -2926,9 +3432,23 @@ var ComponentAdderService = class {
2926
3432
  }
2927
3433
  const componentTypeInPattern = patternDefinition.type;
2928
3434
  let allowedComponentsUpdated = false;
3435
+ const resolvedParentTypes = [];
2929
3436
  for (const parentType of parentTypes) {
2930
- this.logger.info(`Loading parent component: ${parentType}`);
2931
- const { component: parentComponent, filePath: parentComponentFilePath } = await this.componentService.loadComponent(fullComponentsDir, parentType, findOptions);
3437
+ this.logger.info(`Loading component: ${parentType}`);
3438
+ let parentComponent;
3439
+ let parentComponentFilePath;
3440
+ try {
3441
+ const result = await this.componentService.loadComponent(fullComponentsDir, parentType, findOptions);
3442
+ parentComponent = result.component;
3443
+ parentComponentFilePath = result.filePath;
3444
+ } catch (error) {
3445
+ if (error instanceof ComponentNotFoundError) {
3446
+ this.logger.warn(`Component not found: ${parentType} (searched: ${fullComponentsDir})`);
3447
+ continue;
3448
+ }
3449
+ throw error;
3450
+ }
3451
+ resolvedParentTypes.push(parentType);
2932
3452
  if (!parentComponent.slots) {
2933
3453
  parentComponent.slots = [];
2934
3454
  }
@@ -2999,7 +3519,7 @@ var ComponentAdderService = class {
2999
3519
  );
3000
3520
  this.logger.info("");
3001
3521
  this.logger.info(
3002
- `Summary: ${allowedComponentsUpdated ? `${parentTypes.length} component definition(s) updated, ` : ""}${compositionsResult} composition(s), ${compositionPatternsResult} composition pattern(s), ${componentPatternsResult} component pattern(s) updated. ${compositionsResult + compositionPatternsResult + componentPatternsResult} instance(s) added.`
3522
+ `Summary: ${allowedComponentsUpdated ? `${resolvedParentTypes.length} component definition(s) updated, ` : ""}${compositionsResult} composition(s), ${compositionPatternsResult} composition pattern(s), ${componentPatternsResult} component pattern(s) updated. ${compositionsResult + compositionPatternsResult + componentPatternsResult} instance(s) added.`
3003
3523
  );
3004
3524
  return {
3005
3525
  allowedComponentsUpdated,
@@ -3159,74 +3679,17 @@ var ComponentAdderService = class {
3159
3679
  }
3160
3680
  return modified;
3161
3681
  }
3162
- };
3163
-
3164
- // src/cli/commands/add-component.ts
3165
- function createAddComponentCommand() {
3166
- const command = new Command7("add-component");
3167
- command.description("Adds a component instance to existing component slots across all compositions.").option("--parentComponentType <type>", "The component type that owns the slot").option("--slot <slotId>", "The slot ID where the component will be added").option("--newComponentType <type>", "The component type to add").option("--parameters <params>", "Pipe-separated parameter assignments (key1:value1|key2:value2)").hook("preAction", (thisCommand) => {
3168
- const opts = thisCommand.opts();
3169
- const requiredOptions = [
3170
- { name: "parentComponentType", flag: "--parentComponentType" },
3171
- { name: "slot", flag: "--slot" },
3172
- { name: "newComponentType", flag: "--newComponentType" }
3173
- ];
3174
- const missing = requiredOptions.filter((opt) => !opts[opt.name]).map((opt) => opt.flag);
3175
- if (missing.length > 0) {
3176
- console.error(`error: missing required options: ${missing.join(", ")}`);
3177
- process.exit(1);
3178
- }
3179
- }).action(async (opts, cmd) => {
3180
- const globalOpts = cmd.optsWithGlobals();
3181
- const options = {
3182
- ...globalOpts,
3183
- parentComponentType: opts.parentComponentType,
3184
- slot: opts.slot,
3185
- newComponentType: opts.newComponentType,
3186
- parameters: opts.parameters
3187
- };
3188
- const logger = new Logger();
3189
- const fileSystem = new FileSystemService();
3190
- const componentService = new ComponentService(fileSystem);
3191
- const adder = new ComponentAdderService(fileSystem, componentService, logger);
3192
- try {
3193
- const result = await adder.addComponent({
3194
- rootDir: options.rootDir,
3195
- componentsDir: options.componentsDir,
3196
- compositionsDir: options.compositionsDir,
3197
- compositionPatternsDir: options.compositionPatternsDir,
3198
- componentPatternsDir: options.componentPatternsDir,
3199
- parentComponentType: options.parentComponentType,
3200
- slot: options.slot,
3201
- newComponentType: options.newComponentType,
3202
- parameters: options.parameters,
3203
- whatIf: options.whatIf ?? false,
3204
- strict: options.strict ?? false
3205
- });
3206
- logger.success(
3207
- `Added component: ${result.allowedComponentsUpdated ? "1 component definition updated, " : ""}${result.instancesAdded} instance(s) added across ${result.compositionsModified} composition(s), ${result.compositionPatternsModified} composition pattern(s), ${result.componentPatternsModified} component pattern(s)`
3208
- );
3209
- } catch (error) {
3210
- if (error instanceof TransformError) {
3211
- logger.error(error.message);
3212
- process.exit(1);
3213
- }
3214
- throw error;
3215
- }
3216
- });
3217
- return command;
3218
- }
3682
+ };
3219
3683
 
3220
- // src/cli/commands/add-component-pattern.ts
3221
- import { Command as Command8 } from "commander";
3222
- function createAddComponentPatternCommand() {
3223
- const command = new Command8("add-component-pattern");
3224
- command.description("Adds a component pattern instance to existing component slots across all compositions.").option("--parentComponentType <type>", "The component type that owns the slot").option("--slot <slotId>", "The slot ID where the component pattern will be added").option("--componentPatternId <id>", "The component pattern ID to add").hook("preAction", (thisCommand) => {
3684
+ // src/cli/commands/add-component.ts
3685
+ function createAddComponentCommand() {
3686
+ const command = new Command7("add-component");
3687
+ command.description("Adds a component instance to existing component slots across all compositions.").option("--parentComponentType <type>", "The component type that owns the slot").option("--slot <slotId>", "The slot ID where the component will be added").option("--newComponentType <type>", "The component type to add").option("--parameters <params>", "Pipe-separated parameter assignments (key1:value1|key2:value2)").hook("preAction", (thisCommand) => {
3225
3688
  const opts = thisCommand.opts();
3226
3689
  const requiredOptions = [
3227
3690
  { name: "parentComponentType", flag: "--parentComponentType" },
3228
3691
  { name: "slot", flag: "--slot" },
3229
- { name: "componentPatternId", flag: "--componentPatternId" }
3692
+ { name: "newComponentType", flag: "--newComponentType" }
3230
3693
  ];
3231
3694
  const missing = requiredOptions.filter((opt) => !opts[opt.name]).map((opt) => opt.flag);
3232
3695
  if (missing.length > 0) {
@@ -3239,288 +3702,51 @@ function createAddComponentPatternCommand() {
3239
3702
  ...globalOpts,
3240
3703
  parentComponentType: opts.parentComponentType,
3241
3704
  slot: opts.slot,
3242
- componentPatternId: opts.componentPatternId
3705
+ newComponentType: opts.newComponentType,
3706
+ parameters: opts.parameters
3243
3707
  };
3244
3708
  const logger = new Logger();
3245
3709
  const fileSystem = new FileSystemService();
3246
3710
  const componentService = new ComponentService(fileSystem);
3247
3711
  const adder = new ComponentAdderService(fileSystem, componentService, logger);
3248
3712
  try {
3249
- const result = await adder.addComponentPattern({
3713
+ const result = await adder.addComponent({
3250
3714
  rootDir: options.rootDir,
3251
- componentsDir: options.componentsDir,
3252
- compositionsDir: options.compositionsDir,
3253
- compositionPatternsDir: options.compositionPatternsDir,
3254
- componentPatternsDir: options.componentPatternsDir,
3255
- parentComponentType: options.parentComponentType,
3256
- slot: options.slot,
3257
- componentPatternId: options.componentPatternId,
3258
- whatIf: options.whatIf ?? false,
3259
- strict: options.strict ?? false
3260
- });
3261
- logger.success(
3262
- `Added component pattern: ${result.allowedComponentsUpdated ? "1 component definition updated, " : ""}${result.instancesAdded} instance(s) added across ${result.compositionsModified} composition(s), ${result.compositionPatternsModified} composition pattern(s), ${result.componentPatternsModified} component pattern(s)`
3263
- );
3264
- } catch (error) {
3265
- if (error instanceof TransformError) {
3266
- logger.error(error.message);
3267
- process.exit(1);
3268
- }
3269
- throw error;
3270
- }
3271
- });
3272
- return command;
3273
- }
3274
-
3275
- // src/cli/commands/propagate-root-component-slot.ts
3276
- import { Command as Command9 } from "commander";
3277
-
3278
- // src/core/services/slot-propagator.service.ts
3279
- var SlotPropagatorService = class {
3280
- constructor(fileSystem, componentService, compositionService, logger) {
3281
- this.fileSystem = fileSystem;
3282
- this.componentService = componentService;
3283
- this.compositionService = compositionService;
3284
- this.logger = logger;
3285
- }
3286
- async propagate(options) {
3287
- const {
3288
- rootDir,
3289
- componentsDir,
3290
- compositionsDir,
3291
- compositionType,
3292
- slot,
3293
- targetComponentType,
3294
- targetSlot,
3295
- whatIf,
3296
- strict,
3297
- deleteSourceSlot
3298
- } = options;
3299
- const findOptions = { strict };
3300
- const compositionTypes = this.parsePipeSeparatedValues(compositionType, strict);
3301
- const fullComponentsDir = this.fileSystem.resolvePath(rootDir, componentsDir);
3302
- const fullCompositionsDir = this.fileSystem.resolvePath(rootDir, compositionsDir);
3303
- const sourceComponents = [];
3304
- for (const sourceType of compositionTypes) {
3305
- this.logger.info(`Loading component: ${sourceType}`);
3306
- const { component: sourceComponent, filePath: sourceFilePath } = await this.componentService.loadComponent(fullComponentsDir, sourceType, findOptions);
3307
- sourceComponents.push({ sourceType, sourceFilePath, sourceComponent });
3308
- }
3309
- this.logger.info(`Loading component: ${targetComponentType}`);
3310
- const { component: targetComponent, filePath: targetFilePath } = await this.componentService.loadComponent(fullComponentsDir, targetComponentType, findOptions);
3311
- const slotNames = slot.split("|").map((s) => s.trim()).filter((s) => s.length > 0);
3312
- const resolvedSlots = [];
3313
- const resolvedSlotIds = [];
3314
- for (const { sourceType, sourceComponent } of sourceComponents) {
3315
- const notFound = [];
3316
- for (const slotName of slotNames) {
3317
- const slotDef = this.componentService.findSlot(sourceComponent, slotName, findOptions);
3318
- if (!slotDef) {
3319
- notFound.push(slotName);
3320
- continue;
3321
- }
3322
- const exists = resolvedSlots.some(
3323
- (existing) => strict ? existing.id === slotDef.id : existing.id.toLowerCase() === slotDef.id.toLowerCase()
3324
- );
3325
- if (!exists) {
3326
- resolvedSlots.push(slotDef);
3327
- resolvedSlotIds.push(slotDef.id);
3328
- }
3329
- }
3330
- if (notFound.length > 0) {
3331
- throw new PropertyNotFoundError(`Slot "${notFound.join(", ")}"`, sourceType);
3332
- }
3333
- }
3334
- this.logger.info(`Resolved slots: ${resolvedSlotIds.join(", ")}`);
3335
- let modifiedComponent = { ...targetComponent };
3336
- let componentModified = false;
3337
- const existingSlot = this.componentService.findSlot(modifiedComponent, targetSlot, findOptions);
3338
- if (!existingSlot) {
3339
- const mergedAllowedComponents = this.mergeAllowedComponents(resolvedSlots);
3340
- this.logger.action(whatIf, "CREATE", `Slot "${targetSlot}" on ${targetComponentType}`);
3341
- modifiedComponent = this.componentService.addSlot(modifiedComponent, {
3342
- id: targetSlot,
3343
- name: targetSlot,
3344
- allowedComponents: mergedAllowedComponents
3345
- });
3346
- componentModified = true;
3347
- } else {
3348
- const mergedAllowedComponents = this.mergeAllowedComponents([existingSlot, ...resolvedSlots]);
3349
- const existingAllowed = existingSlot.allowedComponents ?? [];
3350
- if (mergedAllowedComponents.length > existingAllowed.length) {
3351
- this.logger.action(
3352
- whatIf,
3353
- "UPDATE",
3354
- `Slot "${targetSlot}" allowedComponents on ${targetComponentType}`
3355
- );
3356
- modifiedComponent = this.componentService.updateSlotAllowedComponents(
3357
- modifiedComponent,
3358
- targetSlot,
3359
- mergedAllowedComponents,
3360
- findOptions
3361
- );
3362
- componentModified = true;
3363
- } else {
3364
- this.logger.info(`Slot "${targetSlot}" already exists on ${targetComponentType}`);
3365
- }
3366
- }
3367
- if (componentModified && !whatIf) {
3368
- await this.componentService.saveComponent(targetFilePath, modifiedComponent);
3369
- }
3370
- const compositions = await this.compositionService.findCompositionsByTypes(
3371
- fullCompositionsDir,
3372
- compositionTypes,
3373
- findOptions
3374
- );
3375
- let modifiedCompositions = 0;
3376
- let propagatedInstances = 0;
3377
- for (const { composition, filePath } of compositions) {
3378
- const rootSlots = composition.composition.slots ?? {};
3379
- const instances = this.compositionService.findComponentInstances(
3380
- composition,
3381
- targetComponentType,
3382
- findOptions
3383
- );
3384
- if (instances.length === 0) {
3385
- continue;
3386
- }
3387
- const componentsToPropagate = [];
3388
- for (const slotDef of resolvedSlots) {
3389
- const slotContent = rootSlots[slotDef.id] ?? [];
3390
- componentsToPropagate.push(...slotContent);
3391
- }
3392
- if (componentsToPropagate.length === 0) {
3393
- continue;
3394
- }
3395
- let compositionModified = false;
3396
- const relativePath = filePath.replace(fullCompositionsDir, "").replace(/^[/\\]/, "");
3397
- const instanceUpdates = [];
3398
- for (const { instance, instanceId } of instances) {
3399
- const instanceName = instance._id ?? instanceId;
3400
- const clonedComponents = this.deepCloneComponents(componentsToPropagate);
3401
- this.addComponentsToInstanceSlot(instance, targetSlot, clonedComponents);
3402
- compositionModified = true;
3403
- propagatedInstances++;
3404
- instanceUpdates.push(
3405
- `${targetComponentType} "${instanceName}": ${componentsToPropagate.length} component(s) \u2192 ${targetSlot}`
3406
- );
3407
- }
3408
- if (compositionModified) {
3409
- this.logger.action(whatIf, "UPDATE", `composition/${relativePath}`);
3410
- for (const update of instanceUpdates) {
3411
- this.logger.detail(`\u2192 ${update}`);
3412
- }
3413
- if (!whatIf) {
3414
- await this.compositionService.saveComposition(filePath, composition);
3415
- }
3416
- modifiedCompositions++;
3417
- }
3418
- }
3419
- let modifiedSourceComponents = 0;
3420
- if (deleteSourceSlot) {
3421
- for (const { sourceType, sourceFilePath, sourceComponent } of sourceComponents) {
3422
- let modifiedSource = { ...sourceComponent };
3423
- let sourceComponentModified = false;
3424
- for (const slotDef of resolvedSlots) {
3425
- const slotToDelete = this.componentService.findSlot(modifiedSource, slotDef.id, findOptions);
3426
- if (!slotToDelete) {
3427
- continue;
3428
- }
3429
- this.logger.action(whatIf, "DELETE", `Slot "${slotDef.id}" from ${sourceType}`);
3430
- modifiedSource = this.componentService.removeSlot(modifiedSource, slotDef.id, findOptions);
3431
- sourceComponentModified = true;
3432
- }
3433
- if (sourceComponentModified) {
3434
- modifiedSourceComponents++;
3435
- }
3436
- if (sourceComponentModified && !whatIf) {
3437
- await this.componentService.saveComponent(sourceFilePath, modifiedSource);
3438
- }
3439
- }
3440
- for (const { composition, filePath } of compositions) {
3441
- const relativePath = filePath.replace(fullCompositionsDir, "").replace(/^[/\\]/, "");
3442
- let cleared = false;
3443
- for (const slotDef of resolvedSlots) {
3444
- if (composition.composition.slots?.[slotDef.id]?.length) {
3445
- composition.composition.slots[slotDef.id] = [];
3446
- cleared = true;
3447
- }
3448
- }
3449
- if (cleared) {
3450
- this.logger.action(whatIf, "CLEAR", `Root slots in composition/${relativePath}`);
3451
- this.logger.detail(`\u2192 Cleared: ${resolvedSlotIds.join(", ")}`);
3452
- if (!whatIf) {
3453
- await this.compositionService.saveComposition(filePath, composition);
3454
- }
3455
- }
3456
- }
3457
- }
3458
- return {
3459
- modifiedComponents: (componentModified ? 1 : 0) + modifiedSourceComponents,
3460
- modifiedCompositions,
3461
- propagatedInstances
3462
- };
3463
- }
3464
- mergeAllowedComponents(slots) {
3465
- const allowed = /* @__PURE__ */ new Set();
3466
- for (const slot of slots) {
3467
- for (const comp of slot.allowedComponents ?? []) {
3468
- allowed.add(comp);
3469
- }
3470
- }
3471
- return Array.from(allowed).sort();
3472
- }
3473
- deepCloneComponents(components) {
3474
- return JSON.parse(JSON.stringify(components));
3475
- }
3476
- addComponentsToInstanceSlot(instance, slotName, components) {
3477
- if (!instance.slots) {
3478
- instance.slots = {};
3479
- }
3480
- if (!instance.slots[slotName]) {
3481
- instance.slots[slotName] = [];
3482
- }
3483
- instance.slots[slotName].push(...components);
3484
- }
3485
- parsePipeSeparatedValues(value, strict) {
3486
- const entries = value.split("|").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
3487
- const normalized = [];
3488
- for (const entry of entries) {
3489
- const exists = normalized.some(
3490
- (existing) => strict ? existing === entry : existing.toLowerCase() === entry.toLowerCase()
3715
+ componentsDir: options.componentsDir,
3716
+ compositionsDir: options.compositionsDir,
3717
+ compositionPatternsDir: options.compositionPatternsDir,
3718
+ componentPatternsDir: options.componentPatternsDir,
3719
+ parentComponentType: options.parentComponentType,
3720
+ slot: options.slot,
3721
+ newComponentType: options.newComponentType,
3722
+ parameters: options.parameters,
3723
+ whatIf: options.whatIf ?? false,
3724
+ strict: options.strict ?? false
3725
+ });
3726
+ logger.success(
3727
+ `Added component: ${result.allowedComponentsUpdated ? "1 component definition updated, " : ""}${result.instancesAdded} instance(s) added across ${result.compositionsModified} composition(s), ${result.compositionPatternsModified} composition pattern(s), ${result.componentPatternsModified} component pattern(s)`
3491
3728
  );
3492
- if (!exists) {
3493
- normalized.push(entry);
3729
+ } catch (error) {
3730
+ if (error instanceof TransformError) {
3731
+ logger.error(error.message);
3732
+ process.exit(1);
3494
3733
  }
3734
+ throw error;
3495
3735
  }
3496
- return normalized;
3497
- }
3498
- };
3736
+ });
3737
+ return command;
3738
+ }
3499
3739
 
3500
- // src/cli/commands/propagate-root-component-slot.ts
3501
- function createPropagateRootComponentSlotCommand() {
3502
- const command = new Command9("propagate-root-component-slot");
3503
- command.description(
3504
- "Copies slot definitions from a composition type's root component to a target component type, then moves all component instances from that slot across all matching compositions."
3505
- ).option(
3506
- "--compositionType <type>",
3507
- "The composition type(s) to process. Supports pipe-separated lists (e.g., HomePage|LandingPage)"
3508
- ).option("--slot <slots>", "Pipe-separated list of slot names to copy from the source component").option(
3509
- "--targetComponentType <type>",
3510
- "The component type that will receive the copied slots"
3511
- ).option(
3512
- "--targetSlot <slot>",
3513
- "The slot name on the target component where the contents will be placed"
3514
- ).option(
3515
- "--deleteSourceSlot",
3516
- "Delete the original slots from the source component after propagation"
3517
- ).hook("preAction", (thisCommand) => {
3740
+ // src/cli/commands/add-component-pattern.ts
3741
+ import { Command as Command8 } from "commander";
3742
+ function createAddComponentPatternCommand() {
3743
+ const command = new Command8("add-component-pattern");
3744
+ command.description("Adds a component pattern instance to existing component slots across all compositions.").option("--parentComponentType <type>", "The component type that owns the slot").option("--slot <slotId>", "The slot ID where the component pattern will be added").option("--componentPatternId <id>", "The component pattern ID to add").hook("preAction", (thisCommand) => {
3518
3745
  const opts = thisCommand.opts();
3519
3746
  const requiredOptions = [
3520
- { name: "compositionType", flag: "--compositionType" },
3747
+ { name: "parentComponentType", flag: "--parentComponentType" },
3521
3748
  { name: "slot", flag: "--slot" },
3522
- { name: "targetComponentType", flag: "--targetComponentType" },
3523
- { name: "targetSlot", flag: "--targetSlot" }
3749
+ { name: "componentPatternId", flag: "--componentPatternId" }
3524
3750
  ];
3525
3751
  const missing = requiredOptions.filter((opt) => !opts[opt.name]).map((opt) => opt.flag);
3526
3752
  if (missing.length > 0) {
@@ -3531,37 +3757,29 @@ function createPropagateRootComponentSlotCommand() {
3531
3757
  const globalOpts = cmd.optsWithGlobals();
3532
3758
  const options = {
3533
3759
  ...globalOpts,
3534
- compositionType: opts.compositionType,
3760
+ parentComponentType: opts.parentComponentType,
3535
3761
  slot: opts.slot,
3536
- targetComponentType: opts.targetComponentType,
3537
- targetSlot: opts.targetSlot,
3538
- deleteSourceSlot: opts.deleteSourceSlot
3762
+ componentPatternId: opts.componentPatternId
3539
3763
  };
3540
3764
  const logger = new Logger();
3541
3765
  const fileSystem = new FileSystemService();
3542
3766
  const componentService = new ComponentService(fileSystem);
3543
- const compositionService = new CompositionService(fileSystem);
3544
- const propagator = new SlotPropagatorService(
3545
- fileSystem,
3546
- componentService,
3547
- compositionService,
3548
- logger
3549
- );
3767
+ const adder = new ComponentAdderService(fileSystem, componentService, logger);
3550
3768
  try {
3551
- const result = await propagator.propagate({
3769
+ const result = await adder.addComponentPattern({
3552
3770
  rootDir: options.rootDir,
3553
3771
  componentsDir: options.componentsDir,
3554
3772
  compositionsDir: options.compositionsDir,
3555
- compositionType: options.compositionType,
3773
+ compositionPatternsDir: options.compositionPatternsDir,
3774
+ componentPatternsDir: options.componentPatternsDir,
3775
+ parentComponentType: options.parentComponentType,
3556
3776
  slot: options.slot,
3557
- targetComponentType: options.targetComponentType,
3558
- targetSlot: options.targetSlot,
3777
+ componentPatternId: options.componentPatternId,
3559
3778
  whatIf: options.whatIf ?? false,
3560
- strict: options.strict ?? false,
3561
- deleteSourceSlot: options.deleteSourceSlot ?? false
3779
+ strict: options.strict ?? false
3562
3780
  });
3563
3781
  logger.success(
3564
- `Modified ${result.modifiedComponents} component(s), ${result.modifiedCompositions} composition(s)`
3782
+ `Added component pattern: ${result.allowedComponentsUpdated ? "1 component definition updated, " : ""}${result.instancesAdded} instance(s) added across ${result.compositionsModified} composition(s), ${result.compositionPatternsModified} composition pattern(s), ${result.componentPatternsModified} component pattern(s)`
3565
3783
  );
3566
3784
  } catch (error) {
3567
3785
  if (error instanceof TransformError) {
@@ -3574,473 +3792,310 @@ function createPropagateRootComponentSlotCommand() {
3574
3792
  return command;
3575
3793
  }
3576
3794
 
3577
- // src/cli/commands/convert-compositions-to-entries.ts
3578
- import { Command as Command10 } from "commander";
3795
+ // src/cli/commands/propagate-root-component-slot.ts
3796
+ import { Command as Command9 } from "commander";
3579
3797
 
3580
- // src/core/services/composition-converter.service.ts
3581
- import * as crypto from "crypto";
3582
- var CompositionConverterService = class {
3798
+ // src/core/services/slot-propagator.service.ts
3799
+ var SlotPropagatorService = class {
3583
3800
  constructor(fileSystem, componentService, compositionService, logger) {
3584
3801
  this.fileSystem = fileSystem;
3585
3802
  this.componentService = componentService;
3586
3803
  this.compositionService = compositionService;
3587
3804
  this.logger = logger;
3588
3805
  }
3589
- async convert(options) {
3806
+ async propagate(options) {
3590
3807
  const {
3591
3808
  rootDir,
3592
- compositionsDir,
3593
3809
  componentsDir,
3594
- contentTypesDir,
3595
- entriesDir,
3596
- compositionTypes,
3597
- flattenComponentIds,
3598
- whatIf,
3599
- strict
3600
- } = options;
3601
- const compositionsDirFull = this.fileSystem.resolvePath(rootDir, compositionsDir);
3602
- const componentsDirFull = this.fileSystem.resolvePath(rootDir, componentsDir);
3603
- const contentTypesDirFull = this.fileSystem.resolvePath(rootDir, contentTypesDir);
3604
- const entriesDirFull = this.fileSystem.resolvePath(rootDir, entriesDir);
3605
- let contentTypesWritten = 0;
3606
- let entriesFromCompositions = 0;
3607
- let entriesFromFlattened = 0;
3608
- let entriesReused = 0;
3609
- this.logger.info(`Composition types: ${compositionTypes.join(", ")}`);
3610
- if (flattenComponentIds.length > 0) {
3611
- this.logger.info(`Flatten component types: ${flattenComponentIds.join(", ")}`);
3612
- }
3613
- const sourceItemMap = flattenComponentIds.length > 0 ? await this.buildSourceItemMap(entriesDirFull) : /* @__PURE__ */ new Map();
3614
- if (sourceItemMap.size > 0) {
3615
- this.logger.info(`Found ${sourceItemMap.size} existing entry(ies) with sourceItem values`);
3616
- }
3617
- const compositionResults = await this.compositionService.findCompositionsByTypes(
3618
- compositionsDirFull,
3619
- compositionTypes,
3620
- { strict }
3621
- );
3622
- if (compositionResults.length === 0) {
3623
- this.logger.warn("No compositions found matching the specified types");
3624
- return { contentTypesWritten: 0, entriesFromCompositions: 0, entriesFromFlattened: 0, entriesReused: 0 };
3625
- }
3626
- this.logger.info(`Found ${compositionResults.length} composition(s)`);
3627
- const rootComponentTypes = /* @__PURE__ */ new Set();
3628
- for (const { composition } of compositionResults) {
3629
- rootComponentTypes.add(composition.composition.type);
3630
- }
3631
- const contentTypeMap = /* @__PURE__ */ new Map();
3632
- for (const rootType of rootComponentTypes) {
3633
- const { component } = await this.componentService.loadComponent(
3634
- componentsDirFull,
3635
- rootType,
3636
- { strict }
3637
- );
3638
- const contentType = this.generateContentType(component);
3639
- contentTypeMap.set(rootType, contentType);
3640
- }
3641
- const flattenContentTypeMap = /* @__PURE__ */ new Map();
3642
- const missingFlattenTypes = [];
3643
- const foundMissingFlattenTypes = /* @__PURE__ */ new Set();
3644
- for (const flattenType of flattenComponentIds) {
3645
- const isRootType = [...rootComponentTypes].some(
3646
- (rt) => this.compareTypes(rt, flattenType, strict)
3647
- );
3648
- if (isRootType) {
3649
- continue;
3650
- }
3651
- try {
3652
- const { component } = await this.componentService.loadComponent(
3653
- componentsDirFull,
3654
- flattenType,
3655
- { strict }
3656
- );
3657
- const contentType = this.generateContentType(component);
3658
- flattenContentTypeMap.set(flattenType, contentType);
3659
- } catch (error) {
3660
- if (error instanceof ComponentNotFoundError) {
3661
- this.logger.info(`Flatten component type not found: ${flattenType}`);
3662
- missingFlattenTypes.push(flattenType);
3663
- continue;
3664
- }
3665
- throw error;
3666
- }
3667
- }
3668
- for (const { composition } of compositionResults) {
3669
- const comp = composition.composition;
3670
- const compositionId = comp._id;
3671
- const compositionName = comp._name ?? compositionId;
3672
- const compositionType = comp.type;
3673
- const entry = this.generateEntryFromComposition(composition);
3674
- const flattenedByType = /* @__PURE__ */ new Map();
3675
- if (flattenComponentIds.length > 0 && comp.slots) {
3676
- for (const flattenType of flattenComponentIds) {
3677
- if (this.compareTypes(flattenType, compositionType, strict)) {
3678
- this.logger.warn(
3679
- `Skipping flatten of "${flattenType}" \u2014 same as root component type`
3680
- );
3681
- continue;
3682
- }
3683
- const instances = this.findFlattenTargets(
3684
- comp.slots,
3685
- flattenType,
3686
- compositionId,
3687
- compositionName,
3688
- strict
3689
- );
3690
- if (instances.length > 0) {
3691
- flattenedByType.set(flattenType, instances);
3692
- if (missingFlattenTypes.includes(flattenType)) {
3693
- foundMissingFlattenTypes.add(flattenType);
3694
- }
3695
- }
3696
- }
3697
- }
3698
- const resolvedRefIds = /* @__PURE__ */ new Map();
3699
- for (const [flattenType, instances] of flattenedByType) {
3700
- const refIds = [];
3701
- for (const inst of instances) {
3702
- const existingId = this.findExistingEntryBySourceItem(inst, sourceItemMap);
3703
- refIds.push(existingId ?? inst.determinisiticId);
3704
- }
3705
- resolvedRefIds.set(flattenType, refIds);
3706
- }
3707
- for (const [flattenType] of flattenedByType) {
3708
- entry.entry.fields[flattenType] = {
3709
- type: "contentReference",
3710
- value: resolvedRefIds.get(flattenType)
3711
- };
3712
- }
3713
- if (flattenComponentIds.length > 0) {
3714
- this.transformContentReferences(entry);
3715
- }
3716
- const entryId = entry.entry._id;
3717
- const entryFilePath = this.fileSystem.joinPath(entriesDirFull, `${entryId}.json`);
3718
- this.logger.action(
3719
- whatIf,
3720
- "WRITE",
3721
- `${entriesDir}/${entryId}.json (${compositionType}, "${this.truncate(compositionName, 50)}")`
3722
- );
3723
- if (!whatIf) {
3724
- await this.fileSystem.writeFile(entryFilePath, entry);
3725
- }
3726
- entriesFromCompositions++;
3727
- for (const [flattenType, instances] of flattenedByType) {
3728
- for (const inst of instances) {
3729
- const existingId = this.findExistingEntryBySourceItem(inst, sourceItemMap);
3730
- if (existingId) {
3731
- this.logger.info(
3732
- `Reusing existing entry ${existingId} for ${flattenType} (sourceItem match)`
3733
- );
3734
- entriesReused++;
3735
- continue;
3736
- }
3737
- const flatEntry = this.generateEntryFromFlattenedInstance(inst);
3738
- const flatEntryPath = this.fileSystem.joinPath(
3739
- entriesDirFull,
3740
- `${inst.determinisiticId}.json`
3741
- );
3742
- this.logger.action(
3743
- whatIf,
3744
- "WRITE",
3745
- `${entriesDir}/${inst.determinisiticId}.json (${flattenType} from "${this.truncate(compositionName, 50)}")`
3746
- );
3747
- if (!whatIf) {
3748
- await this.fileSystem.writeFile(flatEntryPath, flatEntry);
3749
- }
3750
- entriesFromFlattened++;
3751
- }
3752
- }
3810
+ compositionsDir,
3811
+ compositionType,
3812
+ slot,
3813
+ targetComponentType,
3814
+ targetSlot,
3815
+ whatIf,
3816
+ strict,
3817
+ deleteSourceSlot
3818
+ } = options;
3819
+ const findOptions = { strict };
3820
+ const compositionTypes = this.parsePipeSeparatedValues(compositionType, strict);
3821
+ const fullComponentsDir = this.fileSystem.resolvePath(rootDir, componentsDir);
3822
+ const fullCompositionsDir = this.fileSystem.resolvePath(rootDir, compositionsDir);
3823
+ const sourceComponents = [];
3824
+ for (const sourceType of compositionTypes) {
3825
+ this.logger.info(`Loading component: ${sourceType}`);
3826
+ const { component: sourceComponent, filePath: sourceFilePath } = await this.componentService.loadComponent(fullComponentsDir, sourceType, findOptions);
3827
+ sourceComponents.push({ sourceType, sourceFilePath, sourceComponent });
3753
3828
  }
3754
- if (flattenComponentIds.length > 0) {
3755
- for (const contentType of contentTypeMap.values()) {
3756
- for (const flattenType of flattenComponentIds) {
3757
- if (this.compareTypes(flattenType, contentType.id, strict)) {
3758
- continue;
3759
- }
3760
- if (missingFlattenTypes.includes(flattenType) && !foundMissingFlattenTypes.has(flattenType)) {
3761
- continue;
3762
- }
3763
- contentType.fields.push({
3764
- id: flattenType,
3765
- name: flattenType,
3766
- type: "contentReference",
3767
- typeConfig: {
3768
- isMulti: true,
3769
- allowedContentTypes: [flattenType]
3770
- },
3771
- localizable: false
3772
- });
3829
+ this.logger.info(`Loading component: ${targetComponentType}`);
3830
+ const { component: targetComponent, filePath: targetFilePath } = await this.componentService.loadComponent(fullComponentsDir, targetComponentType, findOptions);
3831
+ const slotNames = slot.split("|").map((s) => s.trim()).filter((s) => s.length > 0);
3832
+ const resolvedSlots = [];
3833
+ const resolvedSlotIds = [];
3834
+ for (const { sourceType, sourceComponent } of sourceComponents) {
3835
+ const notFound = [];
3836
+ for (const slotName of slotNames) {
3837
+ const slotDef = this.componentService.findSlot(sourceComponent, slotName, findOptions);
3838
+ if (!slotDef) {
3839
+ notFound.push(slotName);
3840
+ continue;
3841
+ }
3842
+ const exists = resolvedSlots.some(
3843
+ (existing) => strict ? existing.id === slotDef.id : existing.id.toLowerCase() === slotDef.id.toLowerCase()
3844
+ );
3845
+ if (!exists) {
3846
+ resolvedSlots.push(slotDef);
3847
+ resolvedSlotIds.push(slotDef.id);
3773
3848
  }
3774
3849
  }
3775
- }
3776
- for (const [typeName, contentType] of contentTypeMap) {
3777
- const filePath = this.fileSystem.joinPath(contentTypesDirFull, `${typeName}.json`);
3778
- const fieldCount = contentType.fields.filter((f) => f.type !== "contentReference").length;
3779
- const refCount = contentType.fields.filter((f) => f.type === "contentReference").length;
3780
- const refInfo = refCount > 0 ? ` + ${refCount} reference(s)` : "";
3781
- this.logger.action(
3782
- whatIf,
3783
- "WRITE",
3784
- `${contentTypesDir}/${typeName}.json (${fieldCount} fields${refInfo})`
3785
- );
3786
- if (!whatIf) {
3787
- await this.fileSystem.writeFile(filePath, contentType);
3850
+ if (notFound.length > 0) {
3851
+ throw new PropertyNotFoundError(`Slot "${notFound.join(", ")}"`, sourceType);
3788
3852
  }
3789
- contentTypesWritten++;
3790
3853
  }
3791
- for (const [typeName, contentType] of flattenContentTypeMap) {
3792
- const filePath = this.fileSystem.joinPath(contentTypesDirFull, `${typeName}.json`);
3793
- this.logger.action(
3794
- whatIf,
3795
- "WRITE",
3796
- `${contentTypesDir}/${typeName}.json (${contentType.fields.length} fields)`
3797
- );
3798
- if (!whatIf) {
3799
- await this.fileSystem.writeFile(filePath, contentType);
3854
+ this.logger.info(`Resolved slots: ${resolvedSlotIds.join(", ")}`);
3855
+ let modifiedComponent = { ...targetComponent };
3856
+ let componentModified = false;
3857
+ const existingSlot = this.componentService.findSlot(modifiedComponent, targetSlot, findOptions);
3858
+ if (!existingSlot) {
3859
+ const mergedAllowedComponents = this.mergeAllowedComponents(resolvedSlots);
3860
+ this.logger.action(whatIf, "CREATE", `Slot "${targetSlot}" on ${targetComponentType}`);
3861
+ modifiedComponent = this.componentService.addSlot(modifiedComponent, {
3862
+ id: targetSlot,
3863
+ name: targetSlot,
3864
+ allowedComponents: mergedAllowedComponents
3865
+ });
3866
+ componentModified = true;
3867
+ } else {
3868
+ const mergedAllowedComponents = this.mergeAllowedComponents([existingSlot, ...resolvedSlots]);
3869
+ const existingAllowed = existingSlot.allowedComponents ?? [];
3870
+ if (mergedAllowedComponents.length > existingAllowed.length) {
3871
+ this.logger.action(
3872
+ whatIf,
3873
+ "UPDATE",
3874
+ `Slot "${targetSlot}" allowedComponents on ${targetComponentType}`
3875
+ );
3876
+ modifiedComponent = this.componentService.updateSlotAllowedComponents(
3877
+ modifiedComponent,
3878
+ targetSlot,
3879
+ mergedAllowedComponents,
3880
+ findOptions
3881
+ );
3882
+ componentModified = true;
3883
+ } else {
3884
+ this.logger.info(`Slot "${targetSlot}" already exists on ${targetComponentType}`);
3800
3885
  }
3801
- contentTypesWritten++;
3802
3886
  }
3803
- const neverFoundMissingTypes = missingFlattenTypes.filter(
3804
- (type) => !foundMissingFlattenTypes.has(type)
3887
+ if (componentModified && !whatIf) {
3888
+ await this.componentService.saveComponent(targetFilePath, modifiedComponent);
3889
+ }
3890
+ const compositions = await this.compositionService.findCompositionsByTypes(
3891
+ fullCompositionsDir,
3892
+ compositionTypes,
3893
+ findOptions
3805
3894
  );
3806
- if (neverFoundMissingTypes.length > 0) {
3807
- this.logger.warn(
3808
- `Flatten component type(s) not found in any composition: ${neverFoundMissingTypes.join(", ")}`
3895
+ let modifiedCompositions = 0;
3896
+ let propagatedInstances = 0;
3897
+ for (const { composition, filePath } of compositions) {
3898
+ const rootSlots = composition.composition.slots ?? {};
3899
+ const instances = this.compositionService.findComponentInstances(
3900
+ composition,
3901
+ targetComponentType,
3902
+ findOptions
3809
3903
  );
3810
- }
3811
- return { contentTypesWritten, entriesFromCompositions, entriesFromFlattened, entriesReused };
3812
- }
3813
- // --- Content Type Generation ---
3814
- generateContentType(component) {
3815
- const fields = [];
3816
- if (component.parameters) {
3817
- for (const param of component.parameters) {
3818
- fields.push(this.parameterToField(param));
3904
+ if (instances.length === 0) {
3905
+ continue;
3819
3906
  }
3820
- }
3821
- return {
3822
- id: component.id,
3823
- name: component.name,
3824
- fields
3825
- };
3826
- }
3827
- parameterToField(param) {
3828
- const field = {
3829
- id: param.id,
3830
- name: param.name,
3831
- type: param.type
3832
- };
3833
- if (param.helpText !== void 0) {
3834
- field.helpText = param.helpText;
3835
- }
3836
- if (param.localizable !== void 0) {
3837
- field.localizable = param.localizable;
3838
- }
3839
- if (param.typeConfig !== void 0) {
3840
- field.typeConfig = param.typeConfig;
3841
- }
3842
- return field;
3843
- }
3844
- // --- Entry Generation ---
3845
- generateEntryFromComposition(composition) {
3846
- const comp = composition.composition;
3847
- const compositionSpecificKeys = /* @__PURE__ */ new Set(["_id", "_name", "type", "parameters", "slots", "_overrides"]);
3848
- const extraRootProps = {};
3849
- for (const [key, value] of Object.entries(comp)) {
3850
- if (!compositionSpecificKeys.has(key) && value != null) {
3851
- extraRootProps[key] = value;
3907
+ const componentsToPropagate = [];
3908
+ for (const slotDef of resolvedSlots) {
3909
+ const slotContent = rootSlots[slotDef.id] ?? [];
3910
+ componentsToPropagate.push(...slotContent);
3852
3911
  }
3853
- }
3854
- const wrapperKeys = /* @__PURE__ */ new Set(["composition"]);
3855
- const extraWrapperProps = {};
3856
- for (const [key, value] of Object.entries(composition)) {
3857
- if (!wrapperKeys.has(key) && value != null) {
3858
- extraWrapperProps[key] = value;
3912
+ if (componentsToPropagate.length === 0) {
3913
+ continue;
3859
3914
  }
3860
- }
3861
- const entryId = computeGuidHash(`entry${comp._id}`);
3862
- const entryName = this.truncateName(comp._name ?? comp._id, 60);
3863
- return {
3864
- entry: {
3865
- _id: entryId,
3866
- _name: entryName,
3867
- type: comp.type,
3868
- fields: { ...comp.parameters ?? {} },
3869
- ...extraRootProps
3870
- },
3871
- ...extraWrapperProps
3872
- };
3873
- }
3874
- generateEntryFromFlattenedInstance(inst) {
3875
- const entryName = this.truncateName(
3876
- `${inst.componentType} (from ${inst.compositionName})`,
3877
- 60
3878
- );
3879
- return {
3880
- entry: {
3881
- _id: inst.determinisiticId,
3882
- _name: entryName,
3883
- type: inst.componentType,
3884
- fields: { ...inst.instance.parameters ?? {} }
3915
+ let compositionModified = false;
3916
+ const relativePath = filePath.replace(fullCompositionsDir, "").replace(/^[/\\]/, "");
3917
+ const instanceUpdates = [];
3918
+ for (const { instance, instanceId } of instances) {
3919
+ const instanceName = instance._id ?? instanceId;
3920
+ const clonedComponents = regenerateIds(componentsToPropagate, `${instanceName}.${targetSlot}`);
3921
+ this.addComponentsToInstanceSlot(instance, targetSlot, clonedComponents);
3922
+ compositionModified = true;
3923
+ propagatedInstances++;
3924
+ instanceUpdates.push(
3925
+ `${targetComponentType} "${instanceName}": ${componentsToPropagate.length} component(s) \u2192 ${targetSlot}`
3926
+ );
3885
3927
  }
3886
- };
3887
- }
3888
- // --- Flatten Tree Walking ---
3889
- findFlattenTargets(slots, targetType, compositionId, compositionName, strict) {
3890
- const results = [];
3891
- this.walkSlots(slots, targetType, compositionId, compositionName, "", results, strict);
3892
- return results;
3893
- }
3894
- walkSlots(slots, targetType, compositionId, compositionName, pathPrefix, results, strict) {
3895
- for (const [slotName, instances] of Object.entries(slots)) {
3896
- if (!Array.isArray(instances)) continue;
3897
- for (let i = 0; i < instances.length; i++) {
3898
- const instance = instances[i];
3899
- if (instance._pattern) {
3900
- continue;
3928
+ if (compositionModified) {
3929
+ this.logger.action(whatIf, "UPDATE", `composition/${relativePath}`);
3930
+ for (const update of instanceUpdates) {
3931
+ this.logger.detail(`\u2192 ${update}`);
3932
+ }
3933
+ if (!whatIf) {
3934
+ await this.compositionService.saveComposition(filePath, composition);
3935
+ }
3936
+ modifiedCompositions++;
3937
+ }
3938
+ }
3939
+ let modifiedSourceComponents = 0;
3940
+ if (deleteSourceSlot) {
3941
+ for (const { sourceType, sourceFilePath, sourceComponent } of sourceComponents) {
3942
+ let modifiedSource = { ...sourceComponent };
3943
+ let sourceComponentModified = false;
3944
+ for (const slotDef of resolvedSlots) {
3945
+ const slotToDelete = this.componentService.findSlot(modifiedSource, slotDef.id, findOptions);
3946
+ if (!slotToDelete) {
3947
+ continue;
3948
+ }
3949
+ this.logger.action(whatIf, "DELETE", `Slot "${slotDef.id}" from ${sourceType}`);
3950
+ modifiedSource = this.componentService.removeSlot(modifiedSource, slotDef.id, findOptions);
3951
+ sourceComponentModified = true;
3901
3952
  }
3902
- const currentPath = pathPrefix ? `${pathPrefix}-${slotName}-[${i}]-${instance.type}` : `${slotName}-[${i}]-${instance.type}`;
3903
- if (this.compareTypes(instance.type, targetType, strict)) {
3904
- const fullPath = `${compositionId}-${currentPath}`;
3905
- const deterministicId = computeGuidHash(fullPath);
3906
- results.push({
3907
- instance,
3908
- path: fullPath,
3909
- determinisiticId: deterministicId,
3910
- componentType: instance.type,
3911
- compositionId,
3912
- compositionName
3913
- });
3953
+ if (sourceComponentModified) {
3954
+ modifiedSourceComponents++;
3914
3955
  }
3915
- if (instance.slots) {
3916
- this.walkSlots(
3917
- instance.slots,
3918
- targetType,
3919
- compositionId,
3920
- compositionName,
3921
- currentPath,
3922
- results,
3923
- strict
3924
- );
3956
+ if (sourceComponentModified && !whatIf) {
3957
+ await this.componentService.saveComponent(sourceFilePath, modifiedSource);
3925
3958
  }
3926
3959
  }
3927
- }
3928
- }
3929
- // --- Content Reference Transformation ---
3930
- transformContentReferences(entry) {
3931
- const dataResources = {};
3932
- for (const [fieldName, field] of Object.entries(entry.entry.fields)) {
3933
- if (field.type === "contentReference" && Array.isArray(field.value)) {
3934
- const entryIds = field.value;
3935
- const resourceKey = `ref-${entry.entry._id}-${fieldName}`;
3936
- field.value = `\${#jptr:/${resourceKey}/entries}`;
3937
- dataResources[resourceKey] = {
3938
- type: "uniformContentInternalReference",
3939
- variables: {
3940
- locale: "${locale}",
3941
- entryIds: entryIds.join(",")
3960
+ for (const { composition, filePath } of compositions) {
3961
+ const relativePath = filePath.replace(fullCompositionsDir, "").replace(/^[/\\]/, "");
3962
+ let cleared = false;
3963
+ for (const slotDef of resolvedSlots) {
3964
+ if (composition.composition.slots?.[slotDef.id]?.length) {
3965
+ composition.composition.slots[slotDef.id] = [];
3966
+ cleared = true;
3942
3967
  }
3943
- };
3968
+ }
3969
+ if (cleared) {
3970
+ this.logger.action(whatIf, "CLEAR", `Root slots in composition/${relativePath}`);
3971
+ this.logger.detail(`\u2192 Cleared: ${resolvedSlotIds.join(", ")}`);
3972
+ if (!whatIf) {
3973
+ await this.compositionService.saveComposition(filePath, composition);
3974
+ }
3975
+ }
3944
3976
  }
3945
3977
  }
3946
- if (Object.keys(dataResources).length > 0) {
3947
- entry.entry._dataResources = {
3948
- ...entry.entry._dataResources ?? {},
3949
- ...dataResources
3950
- };
3951
- }
3978
+ return {
3979
+ modifiedComponents: (componentModified ? 1 : 0) + modifiedSourceComponents,
3980
+ modifiedCompositions,
3981
+ propagatedInstances
3982
+ };
3952
3983
  }
3953
- // --- Source Item Matching ---
3954
- async buildSourceItemMap(entriesDirFull) {
3955
- const sourceItemMap = /* @__PURE__ */ new Map();
3956
- const entryFiles = await this.fileSystem.findFiles(entriesDirFull, "*.json");
3957
- for (const filePath of entryFiles) {
3958
- try {
3959
- const entryData = await this.fileSystem.readFile(filePath);
3960
- const sourceItemField = entryData?.entry?.fields?.sourceItem;
3961
- if (sourceItemField?.value != null) {
3962
- sourceItemMap.set(String(sourceItemField.value), entryData.entry._id);
3963
- }
3964
- } catch {
3965
- continue;
3984
+ mergeAllowedComponents(slots) {
3985
+ const allowed = /* @__PURE__ */ new Set();
3986
+ for (const slot of slots) {
3987
+ for (const comp of slot.allowedComponents ?? []) {
3988
+ allowed.add(comp);
3966
3989
  }
3967
3990
  }
3968
- return sourceItemMap;
3991
+ return Array.from(allowed).sort();
3969
3992
  }
3970
- findExistingEntryBySourceItem(inst, sourceItemMap) {
3971
- const sourceItemParam = inst.instance.parameters?.sourceItem;
3972
- if (sourceItemParam?.value == null) {
3973
- return void 0;
3974
- }
3975
- return sourceItemMap.get(String(sourceItemParam.value));
3993
+ deepCloneComponents(components) {
3994
+ return JSON.parse(JSON.stringify(components));
3976
3995
  }
3977
- // --- Utilities ---
3978
- compareTypes(type1, type2, strict) {
3979
- if (strict) {
3980
- return type1 === type2;
3996
+ addComponentsToInstanceSlot(instance, slotName, components) {
3997
+ if (!instance.slots) {
3998
+ instance.slots = {};
3981
3999
  }
3982
- return type1.toLowerCase() === type2.toLowerCase();
3983
- }
3984
- truncate(str, maxLength) {
3985
- if (str.length <= maxLength) return str;
3986
- return str.substring(0, maxLength - 3) + "...";
4000
+ if (!instance.slots[slotName]) {
4001
+ instance.slots[slotName] = [];
4002
+ }
4003
+ instance.slots[slotName].push(...components);
3987
4004
  }
3988
- truncateName(name, maxLength) {
3989
- if (name.length <= maxLength) return name;
3990
- return name.substring(0, maxLength);
4005
+ parsePipeSeparatedValues(value, strict) {
4006
+ const entries = value.split("|").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
4007
+ const normalized = [];
4008
+ for (const entry of entries) {
4009
+ const exists = normalized.some(
4010
+ (existing) => strict ? existing === entry : existing.toLowerCase() === entry.toLowerCase()
4011
+ );
4012
+ if (!exists) {
4013
+ normalized.push(entry);
4014
+ }
4015
+ }
4016
+ return normalized;
3991
4017
  }
3992
4018
  };
3993
- function computeGuidHash(guidOrSeed) {
3994
- let uuidStr;
3995
- if (isValidUuid(guidOrSeed)) {
3996
- uuidStr = formatAsBracedUuid(guidOrSeed);
3997
- } else {
3998
- const hash = crypto.createHash("md5").update(guidOrSeed, "utf-8").digest();
3999
- const hex = [
4000
- // First 4 bytes: little-endian in .NET Guid
4001
- hash[3].toString(16).padStart(2, "0"),
4002
- hash[2].toString(16).padStart(2, "0"),
4003
- hash[1].toString(16).padStart(2, "0"),
4004
- hash[0].toString(16).padStart(2, "0"),
4005
- "-",
4006
- // Bytes 4-5: little-endian
4007
- hash[5].toString(16).padStart(2, "0"),
4008
- hash[4].toString(16).padStart(2, "0"),
4009
- "-",
4010
- // Bytes 6-7: little-endian
4011
- hash[7].toString(16).padStart(2, "0"),
4012
- hash[6].toString(16).padStart(2, "0"),
4013
- "-",
4014
- // Bytes 8-9: big-endian
4015
- hash[8].toString(16).padStart(2, "0"),
4016
- hash[9].toString(16).padStart(2, "0"),
4017
- "-",
4018
- // Bytes 10-15: big-endian
4019
- hash[10].toString(16).padStart(2, "0"),
4020
- hash[11].toString(16).padStart(2, "0"),
4021
- hash[12].toString(16).padStart(2, "0"),
4022
- hash[13].toString(16).padStart(2, "0"),
4023
- hash[14].toString(16).padStart(2, "0"),
4024
- hash[15].toString(16).padStart(2, "0")
4025
- ].join("");
4026
- uuidStr = `{${hex}}`.toUpperCase();
4027
- }
4028
- const chars = uuidStr.split("");
4029
- chars[15] = "4";
4030
- const arr20 = ["8", "9", "A", "B"];
4031
- const hexVal = parseInt(chars[20], 16);
4032
- chars[20] = arr20[hexVal % 4];
4033
- return chars.join("").slice(1, -1).toLowerCase();
4034
- }
4035
- function isValidUuid(str) {
4036
- return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(str);
4037
- }
4038
- function formatAsBracedUuid(uuid) {
4039
- const clean = uuid.replace(/[{}]/g, "").toUpperCase();
4040
- return `{${clean}}`;
4019
+
4020
+ // src/cli/commands/propagate-root-component-slot.ts
4021
+ function createPropagateRootComponentSlotCommand() {
4022
+ const command = new Command9("propagate-root-component-slot");
4023
+ command.description(
4024
+ "Copies slot definitions from a composition type's root component to a target component type, then moves all component instances from that slot across all matching compositions."
4025
+ ).option(
4026
+ "--compositionType <type>",
4027
+ "The composition type(s) to process. Supports pipe-separated lists (e.g., HomePage|LandingPage)"
4028
+ ).option("--slot <slots>", "Pipe-separated list of slot names to copy from the source component").option(
4029
+ "--targetComponentType <type>",
4030
+ "The component type that will receive the copied slots"
4031
+ ).option(
4032
+ "--targetSlot <slot>",
4033
+ "The slot name on the target component where the contents will be placed"
4034
+ ).option(
4035
+ "--deleteSourceSlot",
4036
+ "Delete the original slots from the source component after propagation"
4037
+ ).hook("preAction", (thisCommand) => {
4038
+ const opts = thisCommand.opts();
4039
+ const requiredOptions = [
4040
+ { name: "compositionType", flag: "--compositionType" },
4041
+ { name: "slot", flag: "--slot" },
4042
+ { name: "targetComponentType", flag: "--targetComponentType" },
4043
+ { name: "targetSlot", flag: "--targetSlot" }
4044
+ ];
4045
+ const missing = requiredOptions.filter((opt) => !opts[opt.name]).map((opt) => opt.flag);
4046
+ if (missing.length > 0) {
4047
+ console.error(`error: missing required options: ${missing.join(", ")}`);
4048
+ process.exit(1);
4049
+ }
4050
+ }).action(async (opts, cmd) => {
4051
+ const globalOpts = cmd.optsWithGlobals();
4052
+ const options = {
4053
+ ...globalOpts,
4054
+ compositionType: opts.compositionType,
4055
+ slot: opts.slot,
4056
+ targetComponentType: opts.targetComponentType,
4057
+ targetSlot: opts.targetSlot,
4058
+ deleteSourceSlot: opts.deleteSourceSlot
4059
+ };
4060
+ const logger = new Logger();
4061
+ const fileSystem = new FileSystemService();
4062
+ const componentService = new ComponentService(fileSystem);
4063
+ const compositionService = new CompositionService(fileSystem);
4064
+ const propagator = new SlotPropagatorService(
4065
+ fileSystem,
4066
+ componentService,
4067
+ compositionService,
4068
+ logger
4069
+ );
4070
+ try {
4071
+ const result = await propagator.propagate({
4072
+ rootDir: options.rootDir,
4073
+ componentsDir: options.componentsDir,
4074
+ compositionsDir: options.compositionsDir,
4075
+ compositionType: options.compositionType,
4076
+ slot: options.slot,
4077
+ targetComponentType: options.targetComponentType,
4078
+ targetSlot: options.targetSlot,
4079
+ whatIf: options.whatIf ?? false,
4080
+ strict: options.strict ?? false,
4081
+ deleteSourceSlot: options.deleteSourceSlot ?? false
4082
+ });
4083
+ logger.success(
4084
+ `Modified ${result.modifiedComponents} component(s), ${result.modifiedCompositions} composition(s)`
4085
+ );
4086
+ } catch (error) {
4087
+ if (error instanceof TransformError) {
4088
+ logger.error(error.message);
4089
+ process.exit(1);
4090
+ }
4091
+ throw error;
4092
+ }
4093
+ });
4094
+ return command;
4041
4095
  }
4042
4096
 
4043
4097
  // src/cli/commands/convert-compositions-to-entries.ts
4098
+ import { Command as Command10 } from "commander";
4044
4099
  function createConvertCompositionsToEntriesCommand() {
4045
4100
  const command = new Command10("convert-compositions-to-entries");
4046
4101
  command.description(
@@ -4649,7 +4704,7 @@ function createRemoveFieldCommand() {
4649
4704
  // package.json
4650
4705
  var package_default = {
4651
4706
  name: "@uniformdev/transformer",
4652
- version: "1.1.16",
4707
+ version: "1.1.18",
4653
4708
  description: "CLI tool for transforming Uniform.dev serialization files offline",
4654
4709
  type: "module",
4655
4710
  bin: {