From 085547bc8ee42e6976aa6d9ba02644bc00f5a7e3 Mon Sep 17 00:00:00 2001 From: Caleb Porzio Date: Thu, 23 Oct 2025 14:58:08 -0400 Subject: [PATCH 1/5] wip --- src/BladeService.php | 13 +++- src/BlazeServiceProvider.php | 24 +++++-- src/Unblaze.php | 121 +++++++++++++++++++++++++++++++++++ 3 files changed, 151 insertions(+), 7 deletions(-) create mode 100644 src/Unblaze.php diff --git a/src/BladeService.php b/src/BladeService.php index 7edd0c91..a07708b9 100644 --- a/src/BladeService.php +++ b/src/BladeService.php @@ -3,6 +3,7 @@ namespace Livewire\Blaze; use Illuminate\Support\Facades\Event; +use Livewire\Blaze\Unblaze; use ReflectionClass; class BladeService @@ -24,12 +25,22 @@ public function isolatedRender(string $template): string [$compiler, $restore] = $this->freezeObjectProperties($compiler, [ 'rawBlocks', - 'prepareStringsForCompilationUsing' => [], + 'prepareStringsForCompilationUsing' => [ + function ($input) { + if (Unblaze::hasUnblaze($input)) { + $input = Unblaze::processUnblazeDirectives($input); + } + + return $input; + } + ], 'path' => null, ]); try { $result = $compiler->render($template); + + $result = Unblaze::replaceUnblazePrecompiledDirectives($result); } finally { $restore(); $restoreFactory(); diff --git a/src/BlazeServiceProvider.php b/src/BlazeServiceProvider.php index 98b97761..b0a36b5f 100644 --- a/src/BlazeServiceProvider.php +++ b/src/BlazeServiceProvider.php @@ -2,20 +2,21 @@ namespace Livewire\Blaze; -use Livewire\Blaze\Directive\BlazeDirective; -use Livewire\Blaze\Tokenizer\Tokenizer; -use Illuminate\Support\ServiceProvider; -use Livewire\Blaze\Memoizer\Memoizer; use Livewire\Blaze\Walker\Walker; +use Livewire\Blaze\Tokenizer\Tokenizer; use Livewire\Blaze\Parser\Parser; +use Livewire\Blaze\Memoizer\Memoizer; use Livewire\Blaze\Folder\Folder; +use Livewire\Blaze\Directive\BlazeDirective; +use Illuminate\Support\ServiceProvider; +use Illuminate\Support\Facades\Blade; class BlazeServiceProvider extends ServiceProvider { public function register(): void { $this->registerBlazeManager(); - $this->registerBlazeDirectiveFallback(); + $this->registerBlazeDirectiveFallbacks(); $this->registerBladeMacros(); $this->interceptBladeCompilation(); $this->interceptViewCacheInvalidation(); @@ -44,8 +45,19 @@ protected function registerBlazeManager(): void $this->app->bind('blaze', fn ($app) => $app->make(BlazeManager::class)); } - protected function registerBlazeDirectiveFallback(): void + protected function registerBlazeDirectiveFallbacks(): void { + Blade::directive('unblaze', function ($expression) { + return '' + . '<'.'?php $__getScope = fn($scope = []) => $scope; ?>' + . '<'.'?php if (isset($scope)) $__scope = $scope; ?>' + . '<'.'?php $scope = $__getScope('.$expression.'); ?>'; + }); + + Blade::directive('endunblaze', function () { + return '<'.'?php if (isset($__scope)) { $scope = $__scope; unset($__scope); } ?>'; + }); + BlazeDirective::registerFallback(); } diff --git a/src/Unblaze.php b/src/Unblaze.php new file mode 100644 index 00000000..5477edcf --- /dev/null +++ b/src/Unblaze.php @@ -0,0 +1,121 @@ +directive('unblaze', function ($expression) use (&$expressionsByToken) { + $token = str()->random(10); + + $expressionsByToken[$token] = $expression; + + return '[STARTUNBLAZE:'.$token.']'; + }); + + $compiler->directive('endunblaze', function () { + return '[ENDUNBLAZE]'; + }); + + $result = $compiler->compileStatementsMadePublic($template); + + $result = preg_replace_callback('/(\[STARTUNBLAZE:([0-9a-zA-Z]+)\])(.*?)(\[ENDUNBLAZE\])/s', function ($matches) use (&$expressionsByToken) { + $token = $matches[2]; + $expression = $expressionsByToken[$token]; + $innerContent = $matches[3]; + + static::$unblazeReplacements[$token] = $innerContent; + + return '' + . '[STARTCOMPILEDUNBLAZE:'.$token.']' + . '<'.'?php \Livewire\Blaze\Unblaze::storeScope("'.$token.'", '.$expression.') ?>' + . '[ENDCOMPILEDUNBLAZE]'; + }, $result); + + return $result; + } + + public static function replaceUnblazePrecompiledDirectives(string $template) + { + if (str_contains($template, '[STARTCOMPILEDUNBLAZE')) { + $template = preg_replace_callback('/(\[STARTCOMPILEDUNBLAZE:([0-9a-zA-Z]+)\])(.*?)(\[ENDCOMPILEDUNBLAZE\])/s', function ($matches) use (&$expressionsByToken) { + $token = $matches[2]; + + $innerContent = static::$unblazeReplacements[$token]; + + $scope = static::$unblazeScopes[$token]; + + $runtimeScopeString = var_export($scope, true); + + return '' + . '<'.'?php if (isset($scope)) $__scope = $scope; ?>' + . '<'.'?php $scope = '.$runtimeScopeString.'; ?>' + . $innerContent + . '<'.'?php if (isset($__scope)) { $scope = $__scope; unset($__scope); } ?>'; + }, $template); + } + + return $template; + } + + public static function getHackedBladeCompiler() + { + $instance = new class ( + app('files'), + storage_path('framework/views'), + ) extends \Illuminate\View\Compilers\BladeCompiler { + /** + * Make this method public... + */ + public function compileStatementsMadePublic($template) + { + return $this->compileStatements($template); + } + + /** + * Tweak this method to only process custom directives so we + * can restrict rendering solely to @island related directives... + */ + protected function compileStatement($match) + { + if (str_contains($match[1], '@')) { + $match[0] = isset($match[3]) ? $match[1].$match[3] : $match[1]; + } elseif (isset($this->customDirectives[$match[1]])) { + $match[0] = $this->callCustomDirective($match[1], Arr::get($match, 3)); + } elseif (method_exists($this, $method = 'compile'.ucfirst($match[1]))) { + // Don't process through built-in directive methods... + // $match[0] = $this->$method(Arr::get($match, 3)); + + // Just return the original match... + return $match[0]; + } else { + return $match[0]; + } + + return isset($match[3]) ? $match[0] : $match[0].$match[2]; + } + }; + + return $instance; + } +} \ No newline at end of file From 56170a0a23f03c57d3a3035bd95221b7b9fd30ef Mon Sep 17 00:00:00 2001 From: Josh Hanley Date: Fri, 24 Oct 2025 19:07:59 +1000 Subject: [PATCH 2/5] Fix failed render unblaze issue --- src/BladeService.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/BladeService.php b/src/BladeService.php index a07708b9..753e9b4f 100644 --- a/src/BladeService.php +++ b/src/BladeService.php @@ -3,6 +3,7 @@ namespace Livewire\Blaze; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\File; use Livewire\Blaze\Unblaze; use ReflectionClass; @@ -12,6 +13,10 @@ public function isolatedRender(string $template): string { $compiler = app('blade.compiler'); + $temporaryCachePath = storage_path('framework/views/blaze/isolated-render/'); + + File::ensureDirectoryExists($temporaryCachePath); + $factory = app('view'); [$factory, $restoreFactory] = $this->freezeObjectProperties($factory, [ @@ -24,6 +29,7 @@ public function isolatedRender(string $template): string ]); [$compiler, $restore] = $this->freezeObjectProperties($compiler, [ + 'cachePath' => $temporaryCachePath, 'rawBlocks', 'prepareStringsForCompilationUsing' => [ function ($input) { @@ -44,6 +50,8 @@ function ($input) { } finally { $restore(); $restoreFactory(); + + File::deleteDirectory($temporaryCachePath); } return $result; From 52ac5a75ed0f4378aaaba400bfd9129f30fe88e7 Mon Sep 17 00:00:00 2001 From: Caleb Porzio Date: Fri, 24 Oct 2025 09:41:27 -0400 Subject: [PATCH 3/5] wip --- tests/BenchmarkTest.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/BenchmarkTest.php b/tests/BenchmarkTest.php index e52eec36..4712baf6 100644 --- a/tests/BenchmarkTest.php +++ b/tests/BenchmarkTest.php @@ -31,7 +31,11 @@ function clearCache() { $files = glob(__DIR__ . '/../vendor/orchestra/testbench-core/laravel/storage/framework/views/*'); foreach ($files as $file) { if (!str_ends_with($file, '.gitignore')) { - unlink($file); + if (is_dir($file)) { + rmdir($file); + } else { + unlink($file); + } } } } From 0ebb5882c89c525c3bce52b05ca93cfd8a5c0b66 Mon Sep 17 00:00:00 2001 From: Caleb Porzio Date: Fri, 24 Oct 2025 09:53:28 -0400 Subject: [PATCH 4/5] add tests --- tests/UnblazeTest.php | 221 ++++++++++++++++++ .../components/mixed-random.blade.php | 8 + .../components/random-static.blade.php | 5 + .../components/scoped-dynamic.blade.php | 8 + .../components/with-unblaze-scope.blade.php | 8 + .../components/with-unblaze.blade.php | 8 + 6 files changed, 258 insertions(+) create mode 100644 tests/UnblazeTest.php create mode 100644 tests/fixtures/components/mixed-random.blade.php create mode 100644 tests/fixtures/components/random-static.blade.php create mode 100644 tests/fixtures/components/scoped-dynamic.blade.php create mode 100644 tests/fixtures/components/with-unblaze-scope.blade.php create mode 100644 tests/fixtures/components/with-unblaze.blade.php diff --git a/tests/UnblazeTest.php b/tests/UnblazeTest.php new file mode 100644 index 00000000..572b5370 --- /dev/null +++ b/tests/UnblazeTest.php @@ -0,0 +1,221 @@ +anonymousComponentNamespace('', 'x'); + app('blade.compiler')->anonymousComponentPath(__DIR__ . '/fixtures/components'); + + // Add view path for our test pages + View::addLocation(__DIR__ . '/fixtures/pages'); + }); + + it('folds component but preserves unblaze block', function () { + $input = ''; + + $compiled = app('blaze')->compile($input); + + // The component should be folded + expect($compiled)->toContain('
'); + expect($compiled)->toContain('

Static Header

'); + expect($compiled)->toContain('
Static Footer
'); + + // The unblaze block should be preserved as dynamic content + expect($compiled)->toContain('{{ $dynamicValue }}'); + expect($compiled)->not->toContain('compile($input); + + // Static parts should be folded + expect($compiled)->toContain('Static Header'); + expect($compiled)->toContain('Static Footer'); + + // Dynamic parts inside @unblaze should remain dynamic + expect($compiled)->toContain('$dynamicValue'); + }); + + it('handles unblaze with scope parameter', function () { + $input = ' '; + + $compiled = app('blaze')->compile($input); + + // The component should be folded + expect($compiled)->toContain('
'); + expect($compiled)->toContain('

Title

'); + expect($compiled)->toContain('

Static paragraph

'); + + // The scope should be captured and made available + expect($compiled)->toContain('$scope'); + }); + + it('encodes scope into compiled view for runtime access', function () { + $input = ' '; + + $compiled = app('blaze')->compile($input); + + // Should contain PHP code to set up the scope + expect($compiled)->toMatch('/\$scope\s*=\s*array\s*\(/'); + + // The dynamic content should reference $scope + expect($compiled)->toContain('$scope[\'message\']'); + }); + + it('renders unblaze component correctly at runtime', function () { + $template = ' '; + + $rendered = \Illuminate\Support\Facades\Blade::render($template); + + // Static content should be present + expect($rendered)->toContain('
'); + expect($rendered)->toContain('

Title

'); + expect($rendered)->toContain('

Static paragraph

'); + + // Dynamic content should be rendered with the scope value + // Note: The actual rendering of scope variables happens at runtime + expect($rendered)->toContain('class="dynamic"'); + }); + + it('allows punching a hole in static component for dynamic section', function () { + $input = <<<'BLADE' + + Static content here + @unblaze + {{ $dynamicValue }} + @endunblaze + More static content + +BLADE; + + $compiled = app('blaze')->compile($input); + + // Card should be folded + expect($compiled)->toContain('
'); + expect($compiled)->toContain('Static content here'); + expect($compiled)->toContain('More static content'); + + // But dynamic part should be preserved + expect($compiled)->toContain('{{ $dynamicValue }}'); + expect($compiled)->not->toContain(''); + }); + + it('supports multiple unblaze blocks in same component', function () { + $template = <<<'BLADE' +@blaze +
+

Static 1

+ @unblaze + {{ $dynamic1 }} + @endunblaze +

Static 2

+ @unblaze + {{ $dynamic2 }} + @endunblaze +

Static 3

+
+BLADE; + + $compiled = app('blaze')->compile($template); + + // All static parts should be folded + expect($compiled)->toContain('

Static 1

'); + expect($compiled)->toContain('

Static 2

'); + expect($compiled)->toContain('

Static 3

'); + + // Both dynamic parts should be preserved + expect($compiled)->toContain('{{ $dynamic1 }}'); + expect($compiled)->toContain('{{ $dynamic2 }}'); + }); + + it('handles nested components with unblaze', function () { + $input = <<<'BLADE' + + Static Button + @unblaze + {{ $dynamicLabel }} + @endunblaze + +BLADE; + + $compiled = app('blaze')->compile($input); + + // Outer card and static button should be folded + expect($compiled)->toContain('
'); + expect($compiled)->toContain('Static Button'); + + // Dynamic button inside unblaze should be preserved + expect($compiled)->toContain('{{ $dynamicLabel }}'); + }); + + it('static folded content with random strings stays the same between renders', function () { + // First render + $render1 = \Illuminate\Support\Facades\Blade::render(''); + + // Second render + $render2 = \Illuminate\Support\Facades\Blade::render(''); + + // The random string should be the same because it was folded at compile time + expect($render1)->toBe($render2); + + // Verify it contains the static structure + expect($render1)->toContain('class="static-component"'); + expect($render1)->toContain('This should be folded and not change between renders'); + }); + + it('unblazed dynamic content changes between renders while static parts stay the same', function () { + // First render with a value + $render1 = \Illuminate\Support\Facades\Blade::render('', ['dynamicValue' => 'first-value']); + + // Second render with a different value + $render2 = \Illuminate\Support\Facades\Blade::render('', ['dynamicValue' => 'second-value']); + + // Extract the static parts (header and footer with random strings) + preg_match('/

Static Random: (.+?)<\/h1>/', $render1, $matches1); + preg_match('/

Static Random: (.+?)<\/h1>/', $render2, $matches2); + $staticRandom1 = $matches1[1] ?? ''; + $staticRandom2 = $matches2[1] ?? ''; + + // The static random strings should be IDENTICAL (folded at compile time) + expect($staticRandom1)->toBe($staticRandom2); + expect($staticRandom1)->not->toBeEmpty(); + + // But the dynamic parts should be DIFFERENT + expect($render1)->toContain('Dynamic value: first-value'); + expect($render2)->toContain('Dynamic value: second-value'); + expect($render1)->not->toContain('second-value'); + expect($render2)->not->toContain('first-value'); + }); + + it('multiple renders of unblaze component proves folding optimization', function () { + // Render the same template multiple times with different dynamic values + $renders = []; + foreach (['one', 'two', 'three'] as $value) { + $renders[] = \Illuminate\Support\Facades\Blade::render( + '', + ['dynamicValue' => $value] + ); + } + + // Extract the static footer random string from each render + $staticFooters = []; + foreach ($renders as $render) { + preg_match('/