Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions Classes/Command/SchemaCommandController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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;
}
Expand Down
38 changes: 36 additions & 2 deletions Classes/Service/SchemaService.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ class SchemaService
*/
protected ?array $excludedNodeTypeNamePatterns = null;

/**
* @Flow\InjectConfiguration(package="Visol.Neos.ZebraSchemaGenerator", path="abstractNodeTypeNameIncludePatterns")
* @var list<string>|null
*/
protected ?array $abstractNodeTypeNameIncludePatterns = null;

/**
* @Flow\InjectConfiguration(package="Visol.Neos.ZebraSchemaGenerator", path="categoryMarkers")
* @var array<string, string>|null
Expand Down Expand Up @@ -160,6 +166,30 @@ public function isExcludedByNamePattern(string $nodeTypeName): bool
return false;
}

/**
* @return list<string>
*/
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 ?? '';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions Configuration/Settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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