Skip to content
Closed
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 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;
}

Expand All @@ -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;
}
Expand Down
36 changes: 34 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="mixinNodeTypeNamePatterns")
* @var list<string>|null
*/
protected ?array $mixinNodeTypeNamePatterns = null;

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

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