diff --git a/app/resources/views/base-view.blade.php b/app/resources/views/base-view.blade.php new file mode 100644 index 0000000..5d141bc --- /dev/null +++ b/app/resources/views/base-view.blade.php @@ -0,0 +1,3 @@ + + @include('included-view') + \ No newline at end of file diff --git a/app/resources/views/included-view.blade.php b/app/resources/views/included-view.blade.php new file mode 100644 index 0000000..7709ae4 --- /dev/null +++ b/app/resources/views/included-view.blade.php @@ -0,0 +1,10 @@ + + +
+ +

The message is:

+ + +
\ No newline at end of file diff --git a/app/resources/views/regular-view.blade.php b/app/resources/views/regular-view.blade.php new file mode 100644 index 0000000..aa0883f --- /dev/null +++ b/app/resources/views/regular-view.blade.php @@ -0,0 +1,10 @@ + + + + +

The message is:

+ + +
\ No newline at end of file diff --git a/app/routes/web.php b/app/routes/web.php index 7d93d90..1262007 100644 --- a/app/routes/web.php +++ b/app/routes/web.php @@ -21,6 +21,8 @@ return view('welcome'); }); +Route::view('/regular-view', 'regular-view'); +Route::view('/base-view', 'base-view'); Route::view('/anonymous', 'anonymous'); Route::view('/blade-method-callbacks', 'blade-method-callbacks'); Route::view('/blade-method', 'blade-method'); diff --git a/src/ComponentHelper.php b/src/ComponentHelper.php index 9272747..a9c78a8 100644 --- a/src/ComponentHelper.php +++ b/src/ComponentHelper.php @@ -76,10 +76,12 @@ public function getTag(Component|View|string $component): ?string $class = $this->getClass($component); if (! $class) { + $isComponent = $component instanceof Component || Str::contains($component, 'components.'); + return Str::of($this->getAlias($component)) ->replace('.', ' ') ->studly() - ->prepend('SpladeComponent') + ->prepend($isComponent ? 'SpladeComponent' : 'Splade') ->toString(); } diff --git a/src/ExtractVueScriptFromBladeView.php b/src/ExtractVueScriptFromBladeView.php index 6b4cfc4..ef06eff 100644 --- a/src/ExtractVueScriptFromBladeView.php +++ b/src/ExtractVueScriptFromBladeView.php @@ -3,14 +3,19 @@ namespace ProtoneMedia\SpladeCore; use Illuminate\Filesystem\Filesystem; +use Illuminate\Support\Arr; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Process; +use Illuminate\Support\Js; use Illuminate\Support\Str; class ExtractVueScriptFromBladeView { protected readonly string $originalScript; + protected array $viewRootLayoutTags = []; + protected string $viewWithoutScriptTag; protected ScriptParser $scriptParser; @@ -30,12 +35,57 @@ public static function from(string $originalView, array $data, string $bladePath return new static($originalView, $data, $bladePath); } + /** + * Check if the view is a Blade Component. + */ + protected function isComponent(): bool + { + return array_key_exists('spladeBridge', $this->data) + && Str::contains($this->bladePath, '/components/'); + } + + /** + * Check if the view has a ", '') ->trim() ->toString(); + + $this->extractWrappedViewInRootLayout(); + + if (empty($this->viewRootLayoutTags)) { + return; + } + + $this->viewWithoutScriptTag = Str::of($this->viewWithoutScriptTag) + ->after($this->viewRootLayoutTags[0]) + ->beforeLast($this->viewRootLayoutTags[1]) + ->trim() + ->toString(); } /** @@ -170,6 +263,10 @@ protected function splitOriginalView(): void */ protected function replaceComponentMethodLoadingStates(string $script): string { + if (! $this->isComponent()) { + return $script; + } + $methods = ['refreshComponent', ...$this->getBladeFunctions()]; return preg_replace_callback('/(\w+)\.loading/', function ($matches) use ($methods) { @@ -196,10 +293,9 @@ protected function replaceElementRefs(string $script): string */ protected function extractDefinePropsFromScript(): array { - $defaultProps = Collection::make([ - 'spladeBridge' => 'Object', - 'spladeTemplateId' => 'String', - ])->when($this->viewUsesVModel(), fn (Collection $collection) => $collection->put('modelValue', '{}')); + $defaultProps = Collection::make(['spladeTemplateId' => 'String']) + ->when($this->isComponent(), fn (Collection $collection) => $collection->put('spladeBridge', 'Object')) + ->when($this->viewUsesVModel(), fn (Collection $collection) => $collection->put('modelValue', '{}')); $defineProps = $this->scriptParser->getDefineProps($defaultProps->all()); @@ -222,7 +318,7 @@ protected function renderImports(): string ->push('h') ->when($this->needsSpladeBridge(), fn ($collection) => $collection->push('ref')) ->when($this->isRefreshable(), fn ($collection) => $collection->push('inject')) - ->unless(empty($this->getBladeProperties()), fn ($collection) => $collection->push('computed')) + ->when($this->isComponent() && ! empty($this->getBladeProperties()), fn ($collection) => $collection->push('computed')) ->unique() ->sort() ->implode(','); @@ -285,6 +381,10 @@ protected static function renderBladePropertyAsComputedVueProperty(string $prope */ protected function renderBladePropertiesAsComputedVueProperties(): string { + if (! $this->isRefreshable()) { + return ''; + } + $lines = []; foreach ($this->getBladeProperties() as $property) { diff --git a/src/PendingView.php b/src/PendingView.php new file mode 100644 index 0000000..b39c530 --- /dev/null +++ b/src/PendingView.php @@ -0,0 +1,51 @@ +originalView = $originalView; + + return $this; + } + + public function render(string $hash): string + { + $bridge = Js::from(['tag' => $this->tag, 'template_hash' => $hash])->toHtml(); + + if (empty($this->rootLayoutTags)) { + return Blade::render(<< +HTML); + } + + return Blade::render(<<rootLayoutTags[0]} + +{$this->rootLayoutTags[1]} +HTML); + } + + public static function from(string $viewWithoutScript, string $path, array $rootLayoutTags) + { + /** @var ComponentHelper */ + $componentHelper = app(ComponentHelper::class); + + return new static($viewWithoutScript, $componentHelper->getTag($path), $rootLayoutTags); + } +} diff --git a/src/SpladeCoreServiceProvider.php b/src/SpladeCoreServiceProvider.php index 8d53f61..8d9f0a7 100644 --- a/src/SpladeCoreServiceProvider.php +++ b/src/SpladeCoreServiceProvider.php @@ -103,10 +103,6 @@ protected function registerBladeCompiler() $blade->component('dynamic-component', DynamicComponent::class); }); }); - - BladeCompiler::beforeCompilingString( - fn ($value, $data, $path) => ExtractVueScriptFromBladeView::from($value, $data, $path)->handle(app('files')) - ); } protected function registerFactory() diff --git a/src/View/BladeCompiler.php b/src/View/BladeCompiler.php index 74f9995..98add46 100644 --- a/src/View/BladeCompiler.php +++ b/src/View/BladeCompiler.php @@ -2,7 +2,9 @@ namespace ProtoneMedia\SpladeCore\View; +use Illuminate\Support\Str; use Illuminate\View\Compilers\BladeCompiler as BaseBladeCompiler; +use ProtoneMedia\SpladeCore\ExtractVueScriptFromBladeView; class BladeCompiler extends BaseBladeCompiler { @@ -11,30 +13,34 @@ class BladeCompiler extends BaseBladeCompiler */ protected array $data = []; - /** - * Callbacks that are called before compiling the string. - */ - protected static array $beforeCompilingStringCallbacks = []; + public array $hashes = []; - /** - * Registers a callback that is called before compiling the string. - */ - public static function beforeCompilingString(callable $callback): void - { - static::$beforeCompilingStringCallbacks[] = $callback; - } + public array $pendingViews = []; /** * Extract the Vue script from the given template. */ public function compileString($value): string { - foreach (static::$beforeCompilingStringCallbacks as $callback) { - $callback = $callback->bindTo($this, static::class); - $value = $callback($value, $this->data, $this->getPath()); + $service = ExtractVueScriptFromBladeView::from($value, $this->data, $this->getPath()); + + $result = $service->handle($this->files); + + if (is_string($result)) { + return parent::compileString($result); } - return parent::compileString($value); + // TODO: move to CompilerEngine::get() + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 9); + + // prevent leaking the full path + $path = Str::after($trace[8]['file'], base_path()); + + $hash = md5($path.'.'.$trace[8]['line']); + + $this->pendingViews[$hash] = $result->setOriginalView($value); + + return parent::compileString($result->viewWithoutScript); } /** diff --git a/src/View/CompilerEngine.php b/src/View/CompilerEngine.php index c8b67c2..de15631 100644 --- a/src/View/CompilerEngine.php +++ b/src/View/CompilerEngine.php @@ -2,8 +2,11 @@ namespace ProtoneMedia\SpladeCore\View; +use Illuminate\Support\Facades\Blade; +use Illuminate\Support\Str; use Illuminate\View\Engines\CompilerEngine as BaseCompilerEngine; use ProtoneMedia\SpladeCore\ComponentHelper; +use ProtoneMedia\SpladeCore\PendingView; class CompilerEngine extends BaseCompilerEngine { @@ -31,14 +34,36 @@ public function get($path, array $data = []) if (str_contains($path, DIRECTORY_SEPARATOR.'components'.DIRECTORY_SEPARATOR)) { $vueComponent = $this->componentHelper->getTag($path).'.vue'; + // Delete the compiled script if the Vue component is not found, + // for example, when the compiled component is deleted but not + // the compiled template. if (! $this->files->exists(config('splade-core.compiled_scripts').DIRECTORY_SEPARATOR.$vueComponent)) { $this->files->delete($this->getCompiler()->getCompiledPath($path)); } } - return tap( - parent::get($path, $data), - fn () => $compiler->setData([]) - ); + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 6); + + // prevent leaking the full path + $tracePath = Str::after($trace[5]['file'], base_path()); + + $hash = md5($tracePath.'.'.$trace[5]['line']); + + $result = parent::get($path, $data); + $compiler->setData([]); + + // TODO: evaluate if $path contains