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] 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