Skip to content

Commit

Permalink
Merge pull request #20 from Stillat/circular-component-compilation
Browse files Browse the repository at this point in the history
[1.1] Circular component compilation
  • Loading branch information
JohnathonKoster authored Feb 8, 2025
2 parents e8af95b + d9dbe42 commit 2087141
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 8 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Adds compiler support for circular component references, like nested comment threads
- Adds a `#cache` compiler attribute, which may be used to cache the results of any Dagger component
- Bumps the minimum Laravel version to `11.23`, for `Cache::flexible` support
- Improves compilation of custom functions declared within a component's template
Expand Down
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,6 @@ Dagger components are also interopable with Blade components, and will add thems

This is due to the View Manifest. The Dagger compiler and runtime will store which component files were used to create the final output in a JSON file, which is later used for cache-invalidation. The Dagger compiler inlines component templates, which prevents typical file-based cache invalidation from working; the View Manifest solves that problem.

### Are circular component hierarchies supported?

A circular component hierarchy is one where Component A includes Component B, which might conditionally include Component A again. Because the compiler inlines components, circular components are not supported and may result in infinite loops.

### Why build all of this?

Because I wanted to.
Expand Down
16 changes: 16 additions & 0 deletions src/Compiler/Concerns/CompilesComponentDetails.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ protected function assertPrefixIsNotLaravelPrefix(string $componentPrefix): void
throw new InvalidArgumentException('Cannot register [x] as a component prefix.');
}

protected function getComponentHash(ComponentNode $node): string
{
$value = $node->content;

if ($node->isClosedBy != null) {
$value .= $node->innerDocumentContent;
}

return md5($value);
}

protected function getComponentName(ComponentNode $node): string
{
return "{$node->componentPrefix}:{$node->tagName}";
}

/**
* @throws InvalidArgumentException
*/
Expand Down
22 changes: 19 additions & 3 deletions src/Compiler/Concerns/CompilesDynamicComponents.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Illuminate\Support\Str;
use Stillat\BladeParser\Nodes\Components\ComponentNode;
use Stillat\BladeParser\Nodes\Components\ParameterNode;
use Stillat\Dagger\Compiler\ParameterFactory;
use Stillat\Dagger\Facades\SourceMapper;
use Stillat\Dagger\Support\Utils;

Expand Down Expand Up @@ -75,9 +76,24 @@ public function compileDynamicComponent(array $componentDetails, string $compone
return $dynamicComponentPath;
}

protected function compileDynamicComponentScaffolding(ComponentNode $component, string $viewPath): string
protected function compileCircularComponent(ComponentNode $node, string $currentViewPath): string
{
$dynamicComponentName = Utils::makeRandomString();
$circularComponent = clone $node;
$circularComponent->name = 'delegate-component';
$circularComponent->parameters[] = ParameterFactory::parameterFromText('component="'.$node->tagName.'"');

return $this->compileDynamicComponentScaffolding(
$circularComponent,
$currentViewPath,
$this->getComponentHash($node)
);
}

protected function compileDynamicComponentScaffolding(ComponentNode $component, string $viewPath, ?string $dynamicComponentName = null): string
{
if (! $dynamicComponentName) {
$dynamicComponentName = Utils::makeRandomString();
}

$dynamicComponent = clone $component;
$dynamicComponent->parameters = collect($dynamicComponent->parameters)
Expand All @@ -97,7 +113,7 @@ protected function compileDynamicComponentScaffolding(ComponentNode $component,
);
}

$contentDelimiter = '[DYNAMIC::COMPONENT::CONTENT'.Utils::makeRandomString().']';
$contentDelimiter = '[DYNAMIC::COMPONENT::CONTENT'.md5($dynamicComponentName).']';

// Ensure line numbers remain consistent.
$dynamicTemplate = str_repeat($this->newlineStyle, ($component->position?->startLine ?? 1) - 1);
Expand Down
3 changes: 2 additions & 1 deletion src/Compiler/ParameterFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

namespace Stillat\Dagger\Compiler;

use Stillat\BladeParser\Nodes\Components\ParameterFactory as BladeParameterFactory;
use Stillat\BladeParser\Nodes\Components\ParameterNode;
use Stillat\BladeParser\Nodes\Components\ParameterType;

class ParameterFactory
class ParameterFactory extends BladeParameterFactory
{
public static function makeVariableReference(string $variableName, string $value): ParameterNode
{
Expand Down
12 changes: 12 additions & 0 deletions src/Compiler/TemplateCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ final class TemplateCompiler

protected array $componentStack = [];

protected array $activeComponentNames = [];

protected array $componentPath = [];

protected BladeCompiler $compiler;
Expand Down Expand Up @@ -255,6 +257,7 @@ protected function startCompilingComponent(ComponentState $state, array $compile
$this->applyCompilerParameters($state, $compilerParams);

$this->componentStack[] = $state;
$this->activeComponentNames[] = $this->getComponentName($state->node);
$this->activeComponent = $state;

$this->componentPath[] = $state->compilerId;
Expand All @@ -264,6 +267,7 @@ protected function stopCompilingComponent(): void
{
array_pop($this->componentStack);
array_pop($this->componentPath);
array_pop($this->activeComponentNames);

if (count($this->componentStack) > 0) {
$this->activeComponent = $this->componentStack[array_key_last($this->componentStack)];
Expand Down Expand Up @@ -379,6 +383,14 @@ protected function compileNodes(array $nodes): string
return $compiled;
}

$currentComponentName = $this->getComponentName($node);

if (in_array($currentComponentName, $this->activeComponentNames)) {
$compiled .= $this->compileCircularComponent($node, $currentViewPath ?? '');

continue;
}

$varSuffix = Utils::makeRandomString();
$view = $this->manifest->withoutTracing(fn () => $this->resolveView($node));
$sourcePath = $view->getPath();
Expand Down
117 changes: 117 additions & 0 deletions tests/Compiler/CircularComponentsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

use Stillat\Dagger\Tests\CompilerTestCase;

uses(CompilerTestCase::class);

test('it compiles circular component references', function () {
$comments = [
['message' => 'Message 1'],
['message' => 'Message 2'],
[
'message' => 'Message 3',
'comments' => [
['message' => 'Message 3.1'],
[
'message' => 'Message 3.2',
'comments' => [
['message' => 'Message 3.2.1'],
['message' => 'Message 3.2.2'],
[
'message' => 'Message 3.2.3',
'comments' => [
['message' => 'Message 3.2.3.1'],
['message' => 'Message 3.2.3.2'],
['message' => 'Message 3.2.3.3'],
],
],
],
],
['message' => 'Message 3.3'],
],
],
];

$template = <<<'BLADE'
<c-thread :$comments />
BLADE;

$expected = <<<'EXPECTED'
<ul>
<li>
<span>Message 1</span>
</li>
<li>
<span>Message 2</span>
</li>
<li>
<span>Message 3</span>
<ul>
<li>
<span>Message 3.1</span>
</li>
<li>
<span>Message 3.2</span>
<ul>
<li>
<span>Message 3.2.1</span>
</li>
<li>
<span>Message 3.2.2</span>
</li>
<li>
<span>Message 3.2.3</span>
<ul>
<li>
<span>Message 3.2.3.1</span>
</li>
<li>
<span>Message 3.2.3.2</span>
</li>
<li>
<span>Message 3.2.3.3</span>
</li> </ul> </li> </ul> </li>
<li>
<span>Message 3.3</span>
</li> </ul> </li> </ul>
EXPECTED;

$this->assertSame(
$expected,
$this->render($template, ['comments' => $comments])
);
});
9 changes: 9 additions & 0 deletions tests/resources/components/thread/comment.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@props(['comment'])

<li>
<span>{{ $comment['message'] }}</span>

@if (isset($comment['comments']) && count($comment['comments']) > 0)
<c-thread :comments="$comment['comments']" />
@endif
</li>
7 changes: 7 additions & 0 deletions tests/resources/components/thread/index.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@props(['comments'])

<ul>
@foreach ($comments as $comment)
<c-thread.comment :$comment />
@endforeach
</ul>

0 comments on commit 2087141

Please sign in to comment.