diff --git a/Classes/Command/SchemaCommandController.php b/Classes/Command/SchemaCommandController.php index 9d03dd2..333664c 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 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(); + $isIncludedAbstract = $this->schemaService->isAbstractNodeTypeIncluded($name); + + // Abstract types only get an interface when explicitly included + if ($nodeType->isAbstract() && !$isIncludedAbstract) { + 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 exclusion name patterns; + // configured abstract include patterns win over exclusion patterns + if (!$isIncludedAbstract && $this->schemaService->isExcludedByNamePattern($name)) { continue; } @@ -80,7 +89,8 @@ public function generateInterfacesCommand(?string $package = null, bool $forceOv } } - $result = $this->schemaService->buildInterfaceContent($nodeType); + // 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 dace69c..fc05a69 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="abstractNodeTypeNameIncludePatterns") + * @var list|null + */ + protected ?array $abstractNodeTypeNameIncludePatterns = null; + /** * @Flow\InjectConfiguration(package="Visol.Neos.ZebraSchemaGenerator", path="categoryMarkers") * @var array|null @@ -160,6 +166,30 @@ public function isExcludedByNamePattern(string $nodeTypeName): bool return false; } + /** + * @return list + */ + public function getAbstractNodeTypeNameIncludePatterns(): array + { + return $this->abstractNodeTypeNameIncludePatterns ?? []; + } + + /** + * 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 isAbstractNodeTypeIncluded(string $nodeTypeName): bool + { + foreach ($this->getAbstractNodeTypeNameIncludePatterns() as $pattern) { + if (str_contains($nodeTypeName, $pattern)) { + return true; + } + } + return false; + } + public function getRelativeComponentImportPrefix(): string { return $this->relativeComponentImportPrefixSetting ?? ''; @@ -483,9 +513,13 @@ 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 + * abstract node type interfaces (mixins, + * constraints, …), 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 +573,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..de37943 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -49,6 +49,15 @@ Visol: - 'Mixin' - 'Constraint' + # 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: content: ':Content.' diff --git a/README.md b/README.md index 8d81c91..47eaf25 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,29 @@ package's own `Configuration/Settings.yaml` documents the generic defaults; over project-specific keys (`interfacesTargetPath`, `componentTargetPath`, `excludedNodeTypes`, `referenceTypesAsString`, `customPropertyTypes`, `extraProperties`, …) in your site package. +### Interfaces for abstract node types + +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: + abstractNodeTypeNameIncludePatterns: + - ':Mixin.' +``` + +Matching node types get an interface named like any other type +(`Vendor.Site:Mixin.Section` → `VendorSite_MixinSection`) in which **all properties are +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 abstract node types. + ## License MIT