Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -487,6 +488,52 @@ $this->Vite->script([
<?= $this->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
<?php
// Output directly instead of buffering to view blocks
echo $this->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
<!-- In an element file -->
<?= $this->Vite->css(['files' => ['src/component.css'], 'block' => false]) ?>
<div class="component">
<!-- Component content -->
</div>
```

**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
<?php $this->Vite->script(['files' => ['src/app.js']]); ?> <!-- Includes @vite/client -->

// Element
<?php $this->Vite->script(['files' => ['src/element.js']]); ?> <!-- No duplicate client -->
```

**Disable Dependent CSS Injection:**

In production mode, `script()` automatically injects dependent CSS from JS entries. Use `cssBlock => false` to disable this:

```php
<?php
// Only output scripts, skip dependent CSS injection
$this->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.
Expand Down
14 changes: 8 additions & 6 deletions src/Service/AssetService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
101 changes: 87 additions & 14 deletions src/View/Helper/ViteHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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, mixed>|string $options Options or file shorthand
* @param \CakeVite\ValueObject\ViteConfig|array<string, mixed>|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);
Expand All @@ -95,100 +104,141 @@ 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 <link rel="modulepreload" href="...">
$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;
}

/**
* Render CSS tags
*
* 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, mixed>|string $options Options or file shorthand
* @param \CakeVite\ValueObject\ViteConfig|array<string, mixed>|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);

$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;
}

/**
* Convenience method for plugin scripts
*
* 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<string, mixed> $options Options
* @param \CakeVite\ValueObject\ViteConfig|array<string, mixed>|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);
}

/**
* Convenience method for plugin styles
*
* 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<string, mixed> $options Options
* @param \CakeVite\ValueObject\ViteConfig|array<string, mixed>|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);
}

/**
Expand Down Expand Up @@ -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<string, mixed> $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);

Expand All @@ -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;
}

/**
Expand Down
Loading
Loading