From eba1955dda993654119574181941cfc5ce9a1b5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Sch=C3=BCtz?= Date: Sat, 4 Jul 2026 11:05:13 +0200 Subject: [PATCH 1/2] FEATURE: Generate optional-property interfaces for configured mixin node types --- Classes/Command/SchemaCommandController.php | 18 ++++++++--- Classes/Service/SchemaService.php | 36 +++++++++++++++++++-- Configuration/Settings.yaml | 8 +++++ README.md | 22 +++++++++++++ 4 files changed, 78 insertions(+), 6 deletions(-) diff --git a/Classes/Command/SchemaCommandController.php b/Classes/Command/SchemaCommandController.php index 9d03dd2..512c327 100644 --- a/Classes/Command/SchemaCommandController.php +++ b/Classes/Command/SchemaCommandController.php @@ -56,20 +56,29 @@ public function generateInterfacesCommand(?string $package = null, bool $forceOv $this->schemaService->generateBaseTypesFile(); $this->outputLine(" Generated: _types.ts"); - $nodeTypes = $this->nodeTypeManager->getNodeTypes(false); + // Include abstract node types so configured mixins can be generated; + // all other abstract types are skipped below. + $nodeTypes = $this->nodeTypeManager->getNodeTypes(true); $generatedInterfaces = []; $excludedNodeTypes = $this->schemaService->getExcludedNodeTypes(); foreach ($nodeTypes as $nodeType) { $name = $nodeType->getName(); + $isMixin = $this->schemaService->isMixinByNamePattern($name); + + // Abstract types only get an interface when configured as mixins + if ($nodeType->isAbstract() && !$isMixin) { + continue; + } // Skip excluded types if (in_array($name, $excludedNodeTypes, true)) { continue; } - // Skip abstract types matching configured name patterns (Mixins, Constraints) - if ($this->schemaService->isExcludedByNamePattern($name)) { + // Skip types matching configured name patterns (Constraints etc.); + // configured mixin patterns win over exclusion patterns + if (!$isMixin && $this->schemaService->isExcludedByNamePattern($name)) { continue; } @@ -80,7 +89,8 @@ public function generateInterfacesCommand(?string $package = null, bool $forceOv } } - $result = $this->schemaService->buildInterfaceContent($nodeType); + // Mixin interfaces describe a partial shape: all properties optional + $result = $this->schemaService->buildInterfaceContent($nodeType, $isMixin); if ($result === null) { continue; } diff --git a/Classes/Service/SchemaService.php b/Classes/Service/SchemaService.php index dace69c..b6fa7dd 100644 --- a/Classes/Service/SchemaService.php +++ b/Classes/Service/SchemaService.php @@ -68,6 +68,12 @@ class SchemaService */ protected ?array $excludedNodeTypeNamePatterns = null; + /** + * @Flow\InjectConfiguration(package="Visol.Neos.ZebraSchemaGenerator", path="mixinNodeTypeNamePatterns") + * @var list|null + */ + protected ?array $mixinNodeTypeNamePatterns = null; + /** * @Flow\InjectConfiguration(package="Visol.Neos.ZebraSchemaGenerator", path="categoryMarkers") * @var array|null @@ -160,6 +166,29 @@ public function isExcludedByNamePattern(string $nodeTypeName): bool return false; } + /** + * @return list + */ + public function getMixinNodeTypeNamePatterns(): array + { + return $this->mixinNodeTypeNamePatterns ?? []; + } + + /** + * Check whether a node type name matches any configured mixin pattern. + * Matching (usually abstract) node types get an interface with all + * properties optional and are exempt from excludedNodeTypeNamePatterns. + */ + public function isMixinByNamePattern(string $nodeTypeName): bool + { + foreach ($this->getMixinNodeTypeNamePatterns() as $pattern) { + if (str_contains($nodeTypeName, $pattern)) { + return true; + } + } + return false; + } + public function getRelativeComponentImportPrefix(): string { return $this->relativeComponentImportPrefixSetting ?? ''; @@ -483,9 +512,12 @@ public function generateBaseTypesFile(): void /** * Generate interface content for a single node type (without writing to disk) * + * @param bool $forceOptionalProperties Emit every property as optional. Used for + * mixin interfaces, which describe a partial + * shape shared across many node types. * @return array{interfaceName: string, content: string}|null */ - public function buildInterfaceContent(NodeType $nodeType): ?array + public function buildInterfaceContent(NodeType $nodeType, bool $forceOptionalProperties = false): ?array { $nodeTypeName = $nodeType->getName(); $interfaceName = $this->getInterfaceName($nodeTypeName); @@ -539,7 +571,7 @@ public function buildInterfaceContent(NodeType $nodeType): ?array } } - $isOptional = $this->isPropertyOptional($propertyConfig); + $isOptional = $forceOptionalProperties || $this->isPropertyOptional($propertyConfig); $optionalMark = $isOptional ? '?' : ''; $comment = $this->generatePropertyComment($propertyConfig); diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index 904598c..145dbfb 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -49,6 +49,14 @@ Visol: - 'Mixin' - 'Constraint' + # Substrings identifying (usually abstract) mixin node types that should get + # a TypeScript interface of their own. Matching types win over + # excludedNodeTypeNamePatterns, and all their properties are emitted as + # optional: a mixin interface describes a partial shape shared by many node + # types, and consumers must handle nodes created before a property existed. + # Empty by default (no mixin interfaces are generated). + mixinNodeTypeNamePatterns: [] + # Substrings that classify a node type into a Zebra component category. categoryMarkers: content: ':Content.' diff --git a/README.md b/README.md index 8d81c91..cdde0bf 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,28 @@ package's own `Configuration/Settings.yaml` documents the generic defaults; over project-specific keys (`interfacesTargetPath`, `componentTargetPath`, `excludedNodeTypes`, `referenceTypesAsString`, `customPropertyTypes`, `extraProperties`, …) in your site package. +### Mixin interfaces + +By default only non-abstract node types get an interface. When integration code reads +mixin properties shared across many node types (e.g. a `spaceBelow` or `containerWidth` +mixin used by wrapper components), configure the mixin name patterns to generate +interfaces for them as well: + +```yaml +Visol: + Neos: + ZebraSchemaGenerator: + mixinNodeTypeNamePatterns: + - ':Mixin.' +``` + +Matching node types (abstract ones included) get an interface named like any other type +(`Vendor.Site:Mixin.Section` → `VendorSite_MixinSection`) in which **all properties are +optional**, because a mixin interface describes a partial shape and consumers must handle +nodes created before a property existed. Mixin patterns win over +`excludedNodeTypeNamePatterns`, so the default `Mixin` exclusion can stay in place. No +Zebra components are generated for mixins. + ## License MIT From 7002efb70a52016a4037f0c366c5262db72e5172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Sch=C3=BCtz?= Date: Sat, 4 Jul 2026 12:13:14 +0200 Subject: [PATCH 2/2] TASK: Rename mixinNodeTypeNamePatterns to abstractNodeTypeNameIncludePatterns Generalizes the abstract-include feature so it covers any abstract node type (mixins, constraints, ...) rather than being named after mixins only. --- Classes/Command/SchemaCommandController.php | 20 +++++++++---------- Classes/Service/SchemaService.php | 22 +++++++++++---------- Configuration/Settings.yaml | 15 +++++++------- README.md | 21 ++++++++++---------- 4 files changed, 41 insertions(+), 37 deletions(-) diff --git a/Classes/Command/SchemaCommandController.php b/Classes/Command/SchemaCommandController.php index 512c327..333664c 100644 --- a/Classes/Command/SchemaCommandController.php +++ b/Classes/Command/SchemaCommandController.php @@ -56,18 +56,18 @@ public function generateInterfacesCommand(?string $package = null, bool $forceOv $this->schemaService->generateBaseTypesFile(); $this->outputLine(" Generated: _types.ts"); - // Include abstract node types so configured mixins can be generated; - // all other abstract types are skipped below. + // Include abstract node types so configured includes (mixins, constraints, …) + // can be generated; all other abstract types are skipped below. $nodeTypes = $this->nodeTypeManager->getNodeTypes(true); $generatedInterfaces = []; $excludedNodeTypes = $this->schemaService->getExcludedNodeTypes(); foreach ($nodeTypes as $nodeType) { $name = $nodeType->getName(); - $isMixin = $this->schemaService->isMixinByNamePattern($name); + $isIncludedAbstract = $this->schemaService->isAbstractNodeTypeIncluded($name); - // Abstract types only get an interface when configured as mixins - if ($nodeType->isAbstract() && !$isMixin) { + // Abstract types only get an interface when explicitly included + if ($nodeType->isAbstract() && !$isIncludedAbstract) { continue; } @@ -76,9 +76,9 @@ public function generateInterfacesCommand(?string $package = null, bool $forceOv continue; } - // Skip types matching configured name patterns (Constraints etc.); - // configured mixin patterns win over exclusion patterns - if (!$isMixin && $this->schemaService->isExcludedByNamePattern($name)) { + // Skip types matching configured exclusion name patterns; + // configured abstract include patterns win over exclusion patterns + if (!$isIncludedAbstract && $this->schemaService->isExcludedByNamePattern($name)) { continue; } @@ -89,8 +89,8 @@ public function generateInterfacesCommand(?string $package = null, bool $forceOv } } - // Mixin interfaces describe a partial shape: all properties optional - $result = $this->schemaService->buildInterfaceContent($nodeType, $isMixin); + // Included abstract interfaces describe a partial shape: all properties optional + $result = $this->schemaService->buildInterfaceContent($nodeType, $isIncludedAbstract); if ($result === null) { continue; } diff --git a/Classes/Service/SchemaService.php b/Classes/Service/SchemaService.php index b6fa7dd..fc05a69 100644 --- a/Classes/Service/SchemaService.php +++ b/Classes/Service/SchemaService.php @@ -69,10 +69,10 @@ class SchemaService protected ?array $excludedNodeTypeNamePatterns = null; /** - * @Flow\InjectConfiguration(package="Visol.Neos.ZebraSchemaGenerator", path="mixinNodeTypeNamePatterns") + * @Flow\InjectConfiguration(package="Visol.Neos.ZebraSchemaGenerator", path="abstractNodeTypeNameIncludePatterns") * @var list|null */ - protected ?array $mixinNodeTypeNamePatterns = null; + protected ?array $abstractNodeTypeNameIncludePatterns = null; /** * @Flow\InjectConfiguration(package="Visol.Neos.ZebraSchemaGenerator", path="categoryMarkers") @@ -169,19 +169,20 @@ public function isExcludedByNamePattern(string $nodeTypeName): bool /** * @return list */ - public function getMixinNodeTypeNamePatterns(): array + public function getAbstractNodeTypeNameIncludePatterns(): array { - return $this->mixinNodeTypeNamePatterns ?? []; + return $this->abstractNodeTypeNameIncludePatterns ?? []; } /** - * Check whether a node type name matches any configured mixin pattern. - * Matching (usually abstract) node types get an interface with all - * properties optional and are exempt from excludedNodeTypeNamePatterns. + * Check whether a node type name matches any configured abstract include + * pattern (mixins, constraints, …). Matching node types get an interface + * with all properties optional even though they are abstract, and are + * exempt from excludedNodeTypeNamePatterns. */ - public function isMixinByNamePattern(string $nodeTypeName): bool + public function isAbstractNodeTypeIncluded(string $nodeTypeName): bool { - foreach ($this->getMixinNodeTypeNamePatterns() as $pattern) { + foreach ($this->getAbstractNodeTypeNameIncludePatterns() as $pattern) { if (str_contains($nodeTypeName, $pattern)) { return true; } @@ -513,7 +514,8 @@ public function generateBaseTypesFile(): void * Generate interface content for a single node type (without writing to disk) * * @param bool $forceOptionalProperties Emit every property as optional. Used for - * mixin interfaces, which describe a partial + * abstract node type interfaces (mixins, + * constraints, …), which describe a partial * shape shared across many node types. * @return array{interfaceName: string, content: string}|null */ diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index 145dbfb..de37943 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -49,13 +49,14 @@ Visol: - 'Mixin' - 'Constraint' - # Substrings identifying (usually abstract) mixin node types that should get - # a TypeScript interface of their own. Matching types win over - # excludedNodeTypeNamePatterns, and all their properties are emitted as - # optional: a mixin interface describes a partial shape shared by many node - # types, and consumers must handle nodes created before a property existed. - # Empty by default (no mixin interfaces are generated). - mixinNodeTypeNamePatterns: [] + # Substrings identifying abstract node types (mixins, constraints, …) that + # should get a TypeScript interface of their own even though they are + # abstract. Matching types win over excludedNodeTypeNamePatterns, and all + # their properties are emitted as optional: such an interface describes a + # partial shape shared by many node types, and consumers must handle nodes + # created before a property existed. Empty by default (abstract node types + # get no interfaces). + abstractNodeTypeNameIncludePatterns: [] # Substrings that classify a node type into a Zebra component category. categoryMarkers: diff --git a/README.md b/README.md index cdde0bf..47eaf25 100644 --- a/README.md +++ b/README.md @@ -33,27 +33,28 @@ package's own `Configuration/Settings.yaml` documents the generic defaults; over project-specific keys (`interfacesTargetPath`, `componentTargetPath`, `excludedNodeTypes`, `referenceTypesAsString`, `customPropertyTypes`, `extraProperties`, …) in your site package. -### Mixin interfaces +### Interfaces for abstract node types -By default only non-abstract node types get an interface. When integration code reads -mixin properties shared across many node types (e.g. a `spaceBelow` or `containerWidth` -mixin used by wrapper components), configure the mixin name patterns to generate -interfaces for them as well: +By default only non-abstract node types get an interface. With +`abstractNodeTypeNameIncludePatterns`, abstract node types whose name matches one of the +configured substrings are generated as well. The typical case are mixins: integration +code often reads mixin properties shared across many node types (e.g. a `spaceBelow` or +`containerWidth` mixin used by wrapper components) and wants a matching type: ```yaml Visol: Neos: ZebraSchemaGenerator: - mixinNodeTypeNamePatterns: + abstractNodeTypeNameIncludePatterns: - ':Mixin.' ``` -Matching node types (abstract ones included) get an interface named like any other type +Matching node types get an interface named like any other type (`Vendor.Site:Mixin.Section` → `VendorSite_MixinSection`) in which **all properties are -optional**, because a mixin interface describes a partial shape and consumers must handle -nodes created before a property existed. Mixin patterns win over +optional**, because such an interface describes a partial shape and consumers must handle +nodes created before a property existed. Include patterns win over `excludedNodeTypeNamePatterns`, so the default `Mixin` exclusion can stay in place. No -Zebra components are generated for mixins. +Zebra components are generated for abstract node types. ## License