diff --git a/README.md b/README.md index 98c8f21..6e9cc63 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ A [Vite.js](https://vitejs.dev/) integration for CakePHP 5.0+ applications. Seam - [Plugin Assets](#plugin-assets) - [Multiple Entry Points](#multiple-entry-points) - [Custom Attributes](#custom-attributes) + - [Inline Output](#inline-output) - [Preloading Assets](#preloading-assets) - [Caching](#caching) - [Multiple Configurations](#multiple-configurations) @@ -487,6 +488,52 @@ $this->Vite->script([ fetch('custom_scripts') ?> ``` +### Inline Output + +By default, `script()` and `css()` append tags to view blocks for rendering in layouts. +Set `block => false` to return tags as a string for inline output: + +```php +Vite->script(['files' => ['src/main.js'], 'block' => false]); +echo $this->Vite->css(['files' => ['src/style.css'], 'block' => false]); +?> +``` + +This is useful when you need to render assets directly in elements or partials: + +```php + +Vite->css(['files' => ['src/component.css'], 'block' => false]) ?> +
+ +
+``` + +**Vite Client Deduplication:** + +In development mode, the `@vite/client` script is automatically deduplicated. Multiple `script()` calls (e.g., in layout, elements, components) will only output the Vite client once: + +```php +// Layout +Vite->script(['files' => ['src/app.js']]); ?> + +// Element +Vite->script(['files' => ['src/element.js']]); ?> +``` + +**Disable Dependent CSS Injection:** + +In production mode, `script()` automatically injects dependent CSS from JS entries. Use `cssBlock => false` to disable this: + +```php +Vite->script(['files' => ['src/main.js'], 'cssBlock' => false]); +?> +``` + ### Preloading Assets CakeVite supports `modulepreload` to improve load times for applications with code splitting. Preloading hints to the browser which modules will be needed soon, allowing parallel downloads. diff --git a/src/Service/AssetService.php b/src/Service/AssetService.php index 1523262..27ba1a4 100644 --- a/src/Service/AssetService.php +++ b/src/Service/AssetService.php @@ -74,12 +74,14 @@ private function generateDevelopmentScriptTags(ViteConfig $config, array $option { $tags = []; - // Add Vite client - $tags[] = new AssetTag( - url: $config->devServerUrl . '/@vite/client', - type: AssetType::Script, - attributes: ['type' => 'module'], - ); + // Add Vite client (skip if already rendered to avoid duplicates) + if (empty($options['skipViteClient'])) { + $tags[] = new AssetTag( + url: $config->devServerUrl . '/@vite/client', + type: AssetType::Script, + attributes: ['type' => 'module'], + ); + } // Add script entries $entries = $options['files'] ?? $options['devEntries'] ?? $config->scriptEntries; diff --git a/src/View/Helper/ViteHelper.php b/src/View/Helper/ViteHelper.php index b8c25ab..2eae3ac 100644 --- a/src/View/Helper/ViteHelper.php +++ b/src/View/Helper/ViteHelper.php @@ -44,6 +44,11 @@ class ViteHelper extends Helper */ private ?Environment $cachedEnvironment = null; + /** + * Track if @vite/client has been rendered (optimization #3: deduplicate vite client) + */ + private bool $viteClientRendered = false; + /** * Check if currently in development mode * @@ -77,10 +82,14 @@ public function isDev(array|ViteConfig|null $config = null): bool * * Backwards compatible with ViteScriptsHelper::script() * + * When `block` option is `false`, returns the tags as a string for inline output. + * Otherwise appends to the specified view block and returns null. + * * @param array|string $options Options or file shorthand * @param \CakeVite\ValueObject\ViteConfig|array|null $config Configuration + * @return string|null Returns tag string when block is false, null otherwise */ - public function script(array|string $options = [], array|ViteConfig|null $config = null): void + public function script(array|string $options = [], array|ViteConfig|null $config = null): ?string { $options = $this->normalizeOptions($options); $config = $this->extractConfigFromOptions($options, $config); @@ -95,26 +104,46 @@ public function script(array|string $options = [], array|ViteConfig|null $config $block = $options['block'] ?? $config->scriptBlock; $cssBlock = $options['cssBlock'] ?? $config->cssBlock; + $inline = $block === false; + // Pass flag to skip vite client if already rendered (deduplication) + $options['skipViteClient'] = $this->viteClientRendered; $tags = $this->getAssetService()->generateScriptTags($config, $options); + // Mark vite client as rendered after first dev mode script() call + if ($this->isDev($config) && !$this->viteClientRendered) { + $this->viteClientRendered = true; + } + + $output = ''; + // Render tags (preload tags as link elements, script tags as script elements) foreach ($tags as $tag) { if ($tag->isPreload) { // Render preload tags as $preloadType = $tag->preloadType ?? 'modulepreload'; $linkTag = $this->buildPreloadLinkTag($preloadType, $tag->url, $tag->attributes); - $this->getView()->append($block, $linkTag); + if ($inline) { + $output .= $linkTag; + } else { + $this->getView()->append($block, $linkTag); + } } else { // Render regular script tags - $this->Html->script($tag->url, array_merge(['block' => $block], $tag->attributes)); + // When block is false, Html::script() returns the tag string + $scriptTag = $this->Html->script($tag->url, array_merge(['block' => $block], $tag->attributes)); + if ($inline && $scriptTag !== null) { + $output .= $scriptTag; + } } } // Add dependent CSS if in production if (!$this->isDev($config)) { - $this->addDependentCss($config, $options, $cssBlock); + $output .= $this->addDependentCss($config, $options, $cssBlock, $inline); } + + return $inline ? $output : null; } /** @@ -122,10 +151,14 @@ public function script(array|string $options = [], array|ViteConfig|null $config * * Backwards compatible with ViteScriptsHelper::css() * + * When `block` option is `false`, returns the tags as a string for inline output. + * Otherwise appends to the specified view block and returns null. + * * @param array|string $options Options or file shorthand * @param \CakeVite\ValueObject\ViteConfig|array|null $config Configuration + * @return string|null Returns tag string when block is false, null otherwise */ - public function css(array|string $options = [], array|ViteConfig|null $config = null): void + public function css(array|string $options = [], array|ViteConfig|null $config = null): ?string { $options = $this->normalizeOptions($options); $config = $this->extractConfigFromOptions($options, $config); @@ -133,12 +166,21 @@ public function css(array|string $options = [], array|ViteConfig|null $config = $config = $this->resolveConfig($config); $block = $options['block'] ?? $config->cssBlock; + $inline = $block === false; $tags = $this->getAssetService()->generateStyleTags($config, $options); + $output = ''; + foreach ($tags as $tag) { - $this->Html->css($tag->url, array_merge(['block' => $block], $tag->attributes)); + // When block is false, Html::css() returns the tag string + $cssTag = $this->Html->css($tag->url, array_merge(['block' => $block], $tag->attributes)); + if ($inline && $cssTag !== null) { + $output .= $cssTag; + } } + + return $inline ? $output : null; } /** @@ -146,24 +188,28 @@ public function css(array|string $options = [], array|ViteConfig|null $config = * * Backwards compatible with ViteScriptsHelper::pluginScript() * + * When `block` option is `false`, returns the tags as a string for inline output. + * Otherwise appends to the specified view block and returns null. + * * @param string $pluginName Plugin name * @param bool $devMode Development mode flag * @param array $options Options * @param \CakeVite\ValueObject\ViteConfig|array|null $config Configuration + * @return string|null Returns tag string when block is false, null otherwise */ public function pluginScript( string $pluginName, bool $devMode = false, array $options = [], array|ViteConfig|null $config = null, - ): void { + ): ?string { $config = $this->resolveConfig($config); $config = $config->merge([ 'plugin' => $pluginName, 'forceProductionMode' => !$devMode, ]); - $this->script($options, $config); + return $this->script($options, $config); } /** @@ -171,24 +217,28 @@ public function pluginScript( * * Backwards compatible with ViteScriptsHelper::pluginCss() * + * When `block` option is `false`, returns the tags as a string for inline output. + * Otherwise appends to the specified view block and returns null. + * * @param string $pluginName Plugin name * @param bool $devMode Development mode flag * @param array $options Options * @param \CakeVite\ValueObject\ViteConfig|array|null $config Configuration + * @return string|null Returns tag string when block is false, null otherwise */ public function pluginCss( string $pluginName, bool $devMode = false, array $options = [], array|ViteConfig|null $config = null, - ): void { + ): ?string { $config = $this->resolveConfig($config); $config = $config->merge([ 'plugin' => $pluginName, 'forceProductionMode' => !$devMode, ]); - $this->css($options, $config); + return $this->css($options, $config); } /** @@ -270,12 +320,26 @@ private function extractConfigFromOptions( /** * Add dependent CSS from JS entries * + * When cssBlock is false and inline mode is disabled, dependent CSS injection is skipped. + * This allows users to explicitly disable automatic CSS injection. + * * @param \CakeVite\ValueObject\ViteConfig $config Configuration * @param array $options Options - * @param string $cssBlock CSS block name + * @param string|false $cssBlock CSS block name or false to disable/inline + * @param bool $inline Whether to return inline (when script block is false) + * @return string Returns CSS tags when inline is true, empty string otherwise */ - private function addDependentCss(ViteConfig $config, array $options, string $cssBlock): void - { + private function addDependentCss( + ViteConfig $config, + array $options, + string|false $cssBlock, + bool $inline = false, + ): string { + // Skip dependent CSS injection if cssBlock is false and not in inline mode + if ($cssBlock === false && !$inline) { + return ''; + } + $manifestService = new ManifestService(); $manifest = $manifestService->load($config); @@ -287,11 +351,20 @@ private function addDependentCss(ViteConfig $config, array $options, string $css $entries = $manifest->filterEntries(); $pluginPrefix = $config->pluginName ? $config->pluginName . '.' : ''; + $output = ''; + foreach ($entries as $entry) { foreach ($entry->getDependentCssUrls() as $cssUrl) { - $this->Html->css($pluginPrefix . $cssUrl, ['block' => $cssBlock]); + // When inline, pass block => false to get the tag string; otherwise use the block name + $blockOption = $inline ? false : $cssBlock; + $cssTag = $this->Html->css($pluginPrefix . $cssUrl, ['block' => $blockOption]); + if ($inline && $cssTag !== null) { + $output .= $cssTag; + } } } + + return $output; } /** diff --git a/tests/TestCase/View/Helper/ViteHelperTest.php b/tests/TestCase/View/Helper/ViteHelperTest.php index f9c806e..199018a 100644 --- a/tests/TestCase/View/Helper/ViteHelperTest.php +++ b/tests/TestCase/View/Helper/ViteHelperTest.php @@ -727,4 +727,321 @@ public function testBuildPreloadLinkTagEscapesSpecialCharacters(): void $this->assertStringNotContainsString('