diff --git a/src/BladeRenderer.php b/src/BladeRenderer.php
index 89301e0e..b1d10c3c 100644
--- a/src/BladeRenderer.php
+++ b/src/BladeRenderer.php
@@ -8,6 +8,7 @@
use Illuminate\View\Compilers\BladeCompiler;
use Illuminate\View\Component;
use Illuminate\View\ComponentSlot;
+use Livewire\Blaze\Exceptions\StaleUnblazeCacheException;
use Livewire\Blaze\Parser\Attribute;
use Livewire\Blaze\Parser\Nodes\ComponentNode;
use Livewire\Blaze\Parser\Nodes\SlotNode;
@@ -139,7 +140,16 @@ function ($input) {
$restoreRuntime();
}
- $result = Unblaze::replaceUnblazePrecompiledDirectives($result);
+ try {
+ $result = Unblaze::replaceUnblazePrecompiledDirectives($result);
+ } catch (StaleUnblazeCacheException $th) {
+ // The token came from a compiled file cached by an older version of Blaze.
+ // Delete the cache so it gets regenerated on the next compile, and let
+ // the fold be aborted so the component renders normally this time...
+ $this->deleteTemporaryCacheDirectory();
+
+ throw $th;
+ }
return $result;
}
diff --git a/src/BlazeManager.php b/src/BlazeManager.php
index eef12d80..3f0aa9eb 100644
--- a/src/BlazeManager.php
+++ b/src/BlazeManager.php
@@ -21,6 +21,7 @@
use Livewire\Blaze\Support\Directives;
use Livewire\Blaze\Support\ComponentSource;
use Livewire\Blaze\Parser\Nodes\SlotNode;
+use Livewire\Blaze\Parser\Nodes\TextNode;
use Livewire\Blaze\Support\AttributeParser;
class BlazeManager
@@ -79,6 +80,10 @@ public function compile(string $template, ?string $path = null): string
$ast = $this->walker->walk(
nodes: $this->parser->parse($clean),
preCallback: function ($node) use (&$dataStack) {
+ if ($node instanceof ComponentNode) {
+ $node->hasComponentAncestors = $dataStack !== [];
+ }
+
if ($node instanceof ComponentNode && $node->children) {
$dataStack[] = $node->attributes;
@@ -417,10 +422,58 @@ protected function hasAwareDescendant(ComponentNode|SlotNode $node): bool
if ($this->hasAwareDescendant($child)) {
return true;
}
+
+ // Components rendered from within the child's own template aren't
+ // lexically visible here, so we need to scan its source too...
+ if ($this->templateHasAwareComponents($source)) {
+ return true;
+ }
} elseif ($child instanceof SlotNode) {
if ($this->hasAwareDescendant($child)) {
return true;
}
+ } elseif ($child instanceof TextNode) {
+ // Included partials can render components that use @aware...
+ if (preg_match('/@include|@each/', $child->content)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Recursively check if a component's template renders components that use @aware.
+ */
+ protected function templateHasAwareComponents(ComponentSource $source, array &$visited = []): bool
+ {
+ if (! $source->exists() || isset($visited[$source->path])) {
+ return false;
+ }
+
+ $visited[$source->path] = true;
+
+ $content = $source->content();
+
+ // Includes and dynamically resolved components can't be inspected statically...
+ if (preg_match('/@include|@each|delegate-component|dynamic-component/', $content)) {
+ return true;
+ }
+
+ preg_match_all('/<(x-|x:|flux:)([\w.-]+)/', $content, $matches, PREG_SET_ORDER);
+
+ foreach ($matches as $match) {
+ $name = $match[1] === 'flux:' ? 'flux::' . $match[2] : $match[2];
+
+ $childSource = ComponentSource::for($this->blade->componentNameToPath($name));
+
+ if ($childSource->directives->has('aware')) {
+ return true;
+ }
+
+ if ($this->templateHasAwareComponents($childSource, $visited)) {
+ return true;
}
}
diff --git a/src/Exceptions/StaleUnblazeCacheException.php b/src/Exceptions/StaleUnblazeCacheException.php
new file mode 100644
index 00000000..30ffe1ba
--- /dev/null
+++ b/src/Exceptions/StaleUnblazeCacheException.php
@@ -0,0 +1,16 @@
+parentsAttributes[$prop];
}
+
+ // When an @aware prop isn't resolvable from the template itself, the real
+ // parent is only known at runtime — the template may be rendered inside
+ // another component through an @include or a slot — so folding here
+ // would bake in the wrong value...
+ if (! isset($node->attributes[$prop])
+ && ! isset($node->parentsAttributes[$prop])
+ && ! $node->hasComponentAncestors
+ ) {
+ return false;
+ }
}
if (array_key_exists('attributes', $dynamicAttributes)) {
diff --git a/src/Parser/Nodes/ComponentNode.php b/src/Parser/Nodes/ComponentNode.php
index c123ba7d..81a4001c 100644
--- a/src/Parser/Nodes/ComponentNode.php
+++ b/src/Parser/Nodes/ComponentNode.php
@@ -10,6 +10,9 @@ class ComponentNode extends Node
/** Pre-computed by the Walker before children are compiled to TextNodes. */
public bool $hasAwareDescendants = false;
+ /** Whether this node is nested inside another component in the same template. */
+ public bool $hasComponentAncestors = false;
+
public function __construct(
public string $name,
public string $prefix,
diff --git a/src/Unblaze.php b/src/Unblaze.php
index 403a5592..0008bf93 100644
--- a/src/Unblaze.php
+++ b/src/Unblaze.php
@@ -3,6 +3,7 @@
namespace Livewire\Blaze;
use Livewire\Blaze\Compiler\DirectiveCompiler;
+use Livewire\Blaze\Exceptions\StaleUnblazeCacheException;
use Illuminate\Support\Str;
/**
@@ -22,6 +23,14 @@ public static function storeScope($token, $scope = [])
static::$unblazeScopes[$token] = $scope;
}
+ /**
+ * Store the original template content for an @unblaze token.
+ */
+ public static function storeReplacement($token, $content)
+ {
+ static::$unblazeReplacements[$token] = $content;
+ }
+
/**
* Check if a template contains @unblaze directives.
*/
@@ -57,8 +66,13 @@ public static function processUnblazeDirectives(string $template)
static::$unblazeReplacements[$token] = $innerContent;
+ // The replacement is also stored from within the compiled file itself (base64
+ // encoded so it's opaque to later compilation passes) because the file is
+ // cached on disk and may be reused by a process where the in-memory
+ // replacement stored above no longer exists...
return ''
. '[STARTCOMPILEDUNBLAZE:'.$token.']'
+ . '<'.'?php \Livewire\Blaze\Unblaze::storeReplacement("'.$token.'", base64_decode(\''.base64_encode($innerContent).'\')) ?>'
. '<'.'?php \Livewire\Blaze\Unblaze::storeScope("'.$token.'", '.$expression.') ?>'
. '[ENDCOMPILEDUNBLAZE:'.$token.']';
}, $result);
@@ -83,6 +97,10 @@ public static function replaceUnblazePrecompiledDirectives(string $template)
$token = substr($token, 0, -(strlen($trim) + 1));
}
+ if (! array_key_exists($token, static::$unblazeReplacements)) {
+ throw new StaleUnblazeCacheException($token);
+ }
+
$innerContent = Blaze::compileForUnblaze(
static::$unblazeReplacements[$token]
);
diff --git a/tests/ComparisonTest.php b/tests/ComparisonTest.php
index 155bd2a1..f3323d63 100644
--- a/tests/ComparisonTest.php
+++ b/tests/ComparisonTest.php
@@ -52,6 +52,20 @@
BLADE
));
+test('foldable aware through include partial', fn () => compare(<<<'BLADE'
+
+ @include('partials.aware-input')
+
+ BLADE
+));
+
+test('foldable aware through nested component', fn () => compare(<<<'BLADE'
+
+
+
+ BLADE
+));
+
test('foldable boolean attributes', fn () => compare(<<<'BLADE'
BLADE,
diff --git a/tests/FluxProTest.php b/tests/FluxProTest.php
index 79118ee9..f9b4fef9 100644
--- a/tests/FluxProTest.php
+++ b/tests/FluxProTest.php
@@ -34,6 +34,20 @@
BLADE
));
+test('listbox with options emitted from an include partial', fn () => compare(<<<'BLADE'
+
+ @include('partials.listbox-options')
+
+ BLADE
+));
+
+test('listbox with options emitted from a nested component', fn () => compare(<<<'BLADE'
+
+
+
+ BLADE
+));
+
test('chart', fn () => compare(<<<'BLADE'
diff --git a/tests/Folder/FolderTest.php b/tests/Folder/FolderTest.php
index d2f7079f..132a8605 100644
--- a/tests/Folder/FolderTest.php
+++ b/tests/Folder/FolderTest.php
@@ -238,6 +238,26 @@
expect($folded)->toBeInstanceOf(TextNode::class);
});
+test('does not fold components with unresolvable aware props at the template root', function () {
+ // The template may be rendered inside another component at runtime
+ // (through an @include or a slot), so the aware value is unknowable here...
+ $input = '';
+
+ $node = app(Parser::class)->parse($input)[0];
+ $folded = app(Folder::class)->fold($node);
+
+ expect($folded)->toBeInstanceOf(ComponentNode::class);
+});
+
+test('folds components with aware props at the template root when provided directly', function () {
+ $input = '';
+
+ $node = app(Parser::class)->parse($input)[0];
+ $folded = app(Folder::class)->fold($node);
+
+ expect($folded)->toBeInstanceOf(TextNode::class);
+});
+
test('does not fold components with no blaze directive', function () {
$input = '';
diff --git a/tests/Folder/UnblazeTest.php b/tests/Folder/UnblazeTest.php
index 508548e4..9318a0c3 100644
--- a/tests/Folder/UnblazeTest.php
+++ b/tests/Folder/UnblazeTest.php
@@ -38,6 +38,25 @@
);
});
+test('compiles unblaze blocks when the temporary compiled file was cached by a previous process', function () {
+ $input = '';
+
+ $fold = function () use ($input) {
+ $node = app(Parser::class)->parse($input)[0];
+
+ return (new Foldable($node, new ComponentSource(fixture_path('views/components/foldable/input-unblaze.blade.php')), app(BladeRenderer::class), app(BladeService::class)))->fold();
+ };
+
+ $first = $fold();
+
+ // The temporary compiled file persists on disk between processes, but the
+ // unblaze replacements are stored in memory, so we flush them here to
+ // simulate a fold happening in a fresh process reusing the cache...
+ \Livewire\Blaze\Unblaze::flushState();
+
+ expect($fold())->toBe($first);
+});
+
test('folds dynamic attributes used inside unblaze directive', function () {
$input = '';
diff --git a/tests/fixtures/views/components/aware-input-wrapper.blade.php b/tests/fixtures/views/components/aware-input-wrapper.blade.php
new file mode 100644
index 00000000..f12b42e9
--- /dev/null
+++ b/tests/fixtures/views/components/aware-input-wrapper.blade.php
@@ -0,0 +1 @@
+
diff --git a/tests/fixtures/views/components/category-options.blade.php b/tests/fixtures/views/components/category-options.blade.php
new file mode 100644
index 00000000..34bd2bbb
--- /dev/null
+++ b/tests/fixtures/views/components/category-options.blade.php
@@ -0,0 +1,3 @@
+Photography
+Design services
+Create new
diff --git a/tests/fixtures/views/partials/aware-input.blade.php b/tests/fixtures/views/partials/aware-input.blade.php
new file mode 100644
index 00000000..f12b42e9
--- /dev/null
+++ b/tests/fixtures/views/partials/aware-input.blade.php
@@ -0,0 +1 @@
+
diff --git a/tests/fixtures/views/partials/listbox-options.blade.php b/tests/fixtures/views/partials/listbox-options.blade.php
new file mode 100644
index 00000000..34bd2bbb
--- /dev/null
+++ b/tests/fixtures/views/partials/listbox-options.blade.php
@@ -0,0 +1,3 @@
+Photography
+Design services
+Create new